使用实体框架进行原子读写

时间:2015-08-27 20:56:28

标签: sql entity-framework transactions

我有两个不同的进程(在不同的机器上)正在读取和更新数据库记录。

我需要确保的规则是,只有记录的值才能更新记录,例如" Initial"。此外,在提交之后我想知道它是否实际上是从当前进程更新的(如果值不是初始值)

现在,下面的代码执行类似:

var record = context.Records
             .Where(r => (r.id == id && r.State == "Initial"))
             .FirstOrDefault();

if(record != null) {
  record.State = "Second";
  context.SaveChanges();
}

现在有几个问题

1)通过查看代码,看起来在使用州" Initial"获取记录之后,其他一些进程可能已将其更新为状态" Second"在此过程执行SaveChanges之前。 在这种情况下,我们不必要地将状态覆盖到相同的值。这种情况发生在这里吗?

2)如果案例1不是发生的事情,那么EntityFramework可能会将上述内容翻译成类似

的内容
update Record set State = "Second" where Id = someid and State = "Initial"

并将其作为交易执行。这样只有一个进程写入值。这是EF默认的TransactionScope吗?

在这两种情况下,我如何确定更新是从我的流程而不是其他流程进行的?

如果这是内存中的对象,那么在代码中它会转化为假设多个线程访问相同的数据结构

Record rec = FindRecordById(id);
lock (someobject)
{
    if(rec.State == "Initial")
       {
          rec.State = "Second";
          //Now, that I know I updated it I can do some processing
       }
}

由于

1 个答案:

答案 0 :(得分:13)

通常,可以使用两种主要的并发模式:

  • 悲观并发:您锁定一行以防止其他人意外更改您当前尝试更新的数据。 EF 为此类并发模式提供任何本机支持。
  • 乐观并发:引用EF's documentation“乐观并发涉及乐观地尝试将实体保存到数据库,希望自实体以来数据没有发生变化如果事实证明数据已经改变,那么就会抛出异常并且你必须在尝试再次保存之前解决冲突。“ EF支持这种模式,可以相当简单地使用。

关注EF支持的乐观并发选项,让我们比较一下你的例子在EF的乐观并发控制处理方面的行为方式。我假设你使用的是SQL Server。

无并发控制

让我们从数据库中的以下脚本开始:

create table Record (
  Id int identity not null primary key,
  State varchar(50) not null
)

insert into Record (State) values ('Initial')

以下是包含DbContextRecord实体的代码:

public class MyDbContext : DbContext
{
    static MyDbContext()
    {
        Database.SetInitializer<MyDbContext>(null);
    }

    public MyDbContext() : base(@"Server=localhost;Database=eftest;Trusted_Connection=True;") { }

    public DbSet<Record> Records { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Configurations.Add(new Record.Configuration());
    }
}

public class Record
{
    public int Id { get; set; }

    public string State { get; set; }

    public class Configuration : EntityTypeConfiguration<Record>
    {
        public Configuration()
        {
            this.HasKey(t => t.Id);

            this.Property(t => t.State)
                .HasMaxLength(50)
                .IsRequired();
        }
    }
}

现在,让我们使用以下代码测试您的并发更新方案:

static void Main(string[] args)
{
    using (var context = new MyDbContext())
    {
        var record = context.Records
            .Where(r => r.Id == 1 && r.State == "Initial")
            .Single();

        // Insert sneaky update from a different context.
        using (var sneakyContext = new MyDbContext())
        {
            var sneakyRecord = sneakyContext.Records
                .Where(r => r.Id == 1 && r.State == "Initial")
                .Single();

            sneakyRecord.State = "Sneaky Update";
            sneakyContext.SaveChanges();
        }

        // attempt to update row that has just been updated and committed by the sneaky context.
        record.State = "Second";
        context.SaveChanges();
    }
}

如果跟踪SQL,您将看到update语句如下所示:

UPDATE [dbo].[Record]
SET [State] = 'Second'
WHERE ([Id] = 1)

因此,实际上,它并不关心另一个事务是否在更新中潜入。它只是盲目地写了其他更新所做的事情。因此,该行数据库中State的最终值为'Second'

乐观并发控制

让我们调整我们的初始SQL脚本,在表中包含一个并发控制列:

create table Record (
  Id int identity not null primary key,
  State varchar(50) not null,
  Concurrency timestamp not null -- add this row versioning column
)

insert into Record (State) values ('Initial')

我们还调整我们的Record实体类(DbContext类保持不变):

public class Record
{
    public int Id { get; set; }

    public string State { get; set; }

    // Add this property.
    public byte[] Concurrency { get; set; }

    public class Configuration : EntityTypeConfiguration<Record>
    {
        public Configuration()
        {
            this.HasKey(t => t.Id);

            this.Property(t => t.State)
                .HasMaxLength(50)
                .IsRequired();

            // Add this config to tell EF that this
            // property/column should be used for 
            // concurrency checking.
            this.Property(t => t.Concurrency)
                .IsRowVersion();
        }
    }
}

现在,如果我们尝试重新运行我们用于上一个场景的相同Main()方法,您会注意到update语句的生成和执行方式发生了变化:

UPDATE [dbo].[Record]
SET [State] = 'Second'
WHERE (([Id] = 1) AND ([Concurrency] = <byte[]>))
SELECT [Concurrency]
FROM [dbo].[Record]
WHERE @@ROWCOUNT > 0 AND [Id] = 1

特别注意EF如何在where语句的update子句中自动包含为并发控制定义的列。

在这种情况下,因为实际上有并发更新,EF会检测到它,并在此行上抛出DbUpdateConcurrencyException异常:

context.SaveChanges();

因此,在这种情况下,如果您检查数据库,您将看到相关行的State值将为'Sneaky Update',因为我们的第二次更新未能通过并发检查。

最后的想法

正如您所看到的,在EF中激活自动乐观并发控制并不需要做太多工作。

虽然它变得棘手但是,当它被抛出时你如何处理DbUpdateConcurrencyException异常?在这种情况下,很大程度上取决于您决定要做什么。但是有关该主题的进一步指导,您可以在此处找到更多信息:EF - Optimistic Concurrency Patterns