Question

I have two tables, HOTEL and OWNER. Both have and Identity column, but one has a required foreign key for the other table.

I NEED to transactionly add both records at the same time, but if the insert fails on the primary table, I need to rollback the transaction that wrote the record for the secondary table.

As far as I understand I need the .SaveChanges() to fetch the autogenerated ID from the secondary table, but this also appears to be commiting the transaction.

Is there any other way of doing this?

public class HOTEL
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Int64 HOTEL_ID { get; set; }

    public string blah1 { get; set; }

    public string blah2 { get; set; }
}

public class OWNER
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Int64 OWNER_ID { get; set; }

    public string blah3 { get; set; }

    public string blah4 { get; set; }

    public Int64 HOTEL_ID { get; set; }

    [ForeignKey("HOTEL_ID")]
    public virtual HOTEL HOTEL { get; set; }
}

...

public class MyContext : DbContext
{
    public MyContext() : base() { }
    public MyContext(string connectionString) : base(connectionString) { }

    public DbSet<HOTEL> HOTELs { get; set; }
    public DbSet<OWNER> OWNERs { get; set; }

    public ObjectContext ObjectContext
    {
        get
        {
            return (this as IObjectContextAdapter).ObjectContext;
        }
    }
}

...

Int64 ret = 0;

// Suppress required for DB2.
using (var transaction = new TransactionScope(TransactionScopeOption.Suppress))  
{
    try
    {
        using (var context = new MyContext())
        {
            var secondaryEntity = new HOTEL();
            context.HOTELs.Add(secondaryEntity);

            // This appears to commit the changes in the trasaction.
            context.SaveChanges();

            primaryEntity.HOTEL_ID = secondaryEntity.HOTEL_ID;

            context.OWNERs.Attach(primaryEntity);
            context.Entry(primaryEntity).State = primaryEntity.OWNER_ID == 0 ? EntityState.Added : EntityState.Modified;

            context.SaveChanges();
            ret = primaryEntity.OWNER_ID;
        }
    }
    catch (Exception ex)
    {
        // Deal with errors.
    }

    if (ret != 0)
    {
        transaction.Complete();
    }
}

return ret;
Was it helpful?

Solution

After working this over, it turns out the real issue is because I was using the TransactionScopeOption.Suppress. I was using this, because it appeared DB2 wasn't working with any other options. This is because I'm using Client 9.7 Fixpack 4. It was returning the following error when using any other TransactionScopeOption.

ERROR [57016] [IBM][DB2/NT64] SQL0668N  Operation not allowed for reason code "7" on table "DB2ADMIN.HOTEL".

Delving deeper, it is because DB2 isn't partisipating in the TransactionScope. After looking at a few other threds on this issue, it was solved with the following steps.

  • XA transactions enabled in MSDTC properties
    1. Start, Run “dcomcnfg” to start the Component Services Management console.
    2. Navigate the tree to “Component Services > Computers > My Computer > Distributed Transaction Coordinator > Local DTC”.
    3. On “Local DTC”, right click and select Properties.
    4. Select the “Security” Tab.
    5. Check the “Enable XA Transactions” tab.
    6. Press OK.
  • If I continue to use DB2 9.7 FP4, I need to enter a value under the registry key HKLM\SOFTWARE\Microsoft\MSDTC\XADLL with name 'DB2CLI.DLL' and value 'C:\ Program Files \IBM\SQLLIB\BIN\DB2CLI.DLL'

    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSDTC\XADLL] "DB2CLI.DLL"="C:\Program Files\IBM\SQLLIB\BIN\db2cli.dll"

  • Alternatively (from the registry entry), install DB2 Client 9.7 FP6 as a minimum.

  • Reboot computer.
  • Do not close the connection prior to the TransactionScope being disposed.

The final code turned out like this.

Int64 ret = 0;

using (var transaction = new TransactionScope(TransactionScopeOption.Required))
{
    try
    {
        using (var context = new MyContext())
        {
            var secondaryEntity = new HOTEL();
            context.HOTELs.Add(secondaryEntity);

            // SaveChanges(bool) has been depricated, use SaveOptions.
            // SaveChanges is required to generate the autogenerated Identity HOTEL_ID.
            context.ObjectContext.SaveChanges(SaveOptions.AcceptAllChangesAfterSave);
            primaryEntity.HOTEL_ID = secondaryEntity.HOTEL_ID;

            context.OWNERs.Attach(primaryEntity);
            context.Entry(primaryEntity).State = primaryEntity.OWNER_ID == 0 ? EntityState.Added : EntityState.Modified;

            context.ObjectContext.SaveChanges(SaveOptions.AcceptAllChangesAfterSave);
            ret = primaryEntity.OWNER_ID;

            if (ret != 0)
            {
                transaction.Complete();
                context.ObjectContext.AcceptAllChanges();
            }
        }
    }
    catch (Exception ex)
    {
        // Deal with errors.
    }
}

return ret;

An alternative after reading comments from @Pawel, which would generate the secondaryEntity's ID and attach it automatically to the primaryEntity without needing to assign the value manually:

Int64 ret = 0;

using (var transaction = new TransactionScope(TransactionScopeOption.Required))
{
    try
    {
        using (var context = new MyContext())
        {
            var secondaryEntity = new HOTEL();
            primaryEntity.HOTEL = secondaryEntity;

            context.HOTELs.Add(secondaryEntity);
            context.OWNERs.Attach(primaryEntity);
            context.Entry(primaryEntity).State = primaryEntity.OWNER_ID == 0 ? EntityState.Added : EntityState.Modified;

            context.SaveChanges();
            ret = primaryEntity.OWNER_ID;

            // TransactionScopeOption.Required can still used in case something 
            // goes wrong with additional processing at this point.

            if (ret != 0)
            {
                transaction.Complete();
            }
        }
    }
    catch (Exception ex)
    {
        // Deal with errors.
    }
}

return ret;
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top