简体   繁体   中英

Entity Framework baseline integrity

I am working with Entity Framework database first, and I have a table that stores historical values using baseline ids. We store parent/child links on this table using the baseline id. The following columns makeup this design:-

  • Id int (primary key - unique)
  • BaselineId int (not unique, but does uniquely identity revisions of the same item)
  • ParentBaselineId int nullable (refers to the baseline of the linked entity, no FK)
  • Latest bit (indicated that this is the most recent baseline in a series)

Example data for clarity

Id  BaselineId  ParentBaselineId Latest
1   1           NULL             0
2   1           NULL             1
3   2           1                0
4   2           1                1

This shows two items, each with two revisions. Baseline 1 is the parent of baseline 2.

My issue is that for the reasons listed below I lookup the next available baseline in C# and manually specify the BaselineId/ParentBaselineId to be saved. When two users trigger this method at the same time, they save the same Baseline ids, as the save does not complete before the second users looks up the next available baseline id.

  • The method can add many items at once that must be linked together by baseline ids
  • This must be a single SQL transaction so it can rollback completely on error
  • SQL trigger cannot be used to set the baselines because they are needed ahead of time to indicate the relationships

What measures can I take to ensure that the same baseline won't be used by two users running the method at the same time?

My C# looks something like this

using (var tx = new TransactionScope())
{
    using (var context = new DbContext(connectionString))
    {
        int baseline = context.MyTable.Max(e => e.BaselineId);
        context.MyTable.Add(new MyTable() {BaselineId = baseline + 1, Latest = true});
        context.MyTable.Add(new MyTable() { BaselineId = baseline + 2, ParentBaselineId = baseline + 1, Latest = true });
        context.SaveChanges();
    }

    tx.Complete();
}

Using @Steve Greene 's suggestion, I was able to use an SQL sequence. After creating a new sequence in my database and setting the start value to match my existing data, I updated my code to the following.

public long NextBaseline(DbContext context)
{
    DataTable dt = new DataTable();
    var conn = context.Database.Connection;
    var connectionState = conn.State;
    try
    {
        if (connectionState != ConnectionState.Open)
            conn.Open();
        using (var cmd = conn.CreateCommand())
        {
            cmd.CommandText = "SELECT NEXT VALUE FOR MySequence;";
            using (var reader = cmd.ExecuteReader())
            {
                dt.Load(reader);
            }
        }
    }
    catch (Exception ex)
    {
        throw new HCSSException(ex.Message, ex);
    }
    finally
    {
        if (connectionState != ConnectionState.Open)
            conn.Close();
    }
    return Convert.ToInt64(dt.AsEnumerable().First().ItemArray[0]);
}

public void Save()
{
    using (var tx = new TransactionScope())
    {
        using (var context = new DbContext(connectionString))
        {
            var parent = new MyTable() { BaselineId = NextBaseline(context), Latest = true };
            var child = new MyTable() { BaselineId = NextBaseline(context), ParentBaselineId = parent.BaselineId, Latest = true }
            context.MyTable.Add(parent);
            context.MyTable.Add(child);
            context.SaveChanges();
        }

        tx.Complete();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM