小编典典

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

sql

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

我需要确保的规则是,仅当记录的值(假设是“
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
       }
}

谢谢


阅读 209

收藏
2021-04-22

共1个答案

小编典典

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

  • 悲观并发 :您锁定一行以防止其他人意外更改您当前尝试更新的数据。EF并 没有 提供这种类型的并发模式的任何原生支持。
  • 乐观并发 :引用EF文档“乐观并发涉及乐观地尝试将您的实体保存到数据库,以希望自从实体加载以来那里的数据没有改变。如果事实证明数据已更改,则出现异常抛出,您必须先解决冲突,然后再尝试保存。” 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如何自动whereupdate语句的子句中包括为并发控制定义的列。

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

context.SaveChanges();

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

最后的想法

如您所见,在EF中激活自动乐观并发控制不需要做太多的事情。

但是,它变得棘手的地方是,如何DbUpdateConcurrencyException在抛出异常时处理异常?在这种情况下,完全由您决定要做什么。但是,有关该主题的进一步指导,您可以在这里找到更多信息:EF-
乐观并发模式

2021-04-22