使用实体框架进行原子读写 [英] Atomic Read and Write with Entity Framework

查看:63
本文介绍了使用实体框架进行原子读写的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



我需要确保的规则是,仅当记录的值是 Initial时才必须更新记录。另外,提交之后,我想知道它是否实际上已从当前进程中更新(如果值不是初始值)



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

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

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

现在有几个问题



1)从代码中可以看到,在以初始状态获取记录之后,其他一些进程可能已将其更新为第二状态,然后该进程才能执行SaveChanges。
在这种情况下,我们不必要地将状态重写为相同的值。这种情况在这里发生吗?



2)如果情况1没有发生,则EntityFramework可能会将上述内容翻译为

  update记录集State = Second,Id = someid,State = Initial 

并将其作为事务执行。这样,只有一个进程写入该值。 EF默认TransactionScope是这种情况吗?



在这两种情况下,我又如何才能确定是从我的流程而不是其他流程进行了更新? / p>

如果这是内存中对象,则在代码中将转换为假设多个线程访问相同数据结构的内容

 记录rec = FindRecordById(id); 
锁(某对象)
{
if(rec.State == Initial)
{
rec.State = Second;
//现在,我知道我已对其进行更新,我可以进行一些处理
}
}

谢谢

解决方案

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




  • 悲观并发:您锁定一行以防止其他人意外更改当前尝试更新的数据。 EF不会为这种并发模式提供任何本机支持。

  • 乐观并发:援引 EF的文档乐观并发涉及乐观地尝试将您的实体保存到数据库,以期自从实体加载以来,那里的数据没有改变。如果事实证明数据已经改变,那么将引发异常,您必须解决冲突,然后再尝试保存。 EF支持此模式,



关注EF支持的乐观并发选项,让我们比较一下您的示例与和没有EF的乐观并发控制处理。我假设您正在使用SQL Server。



没有并发控制



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

 创建表记录(
ID int身份不为空的主键,
状态varchar(50)不为空


插入记录(状态)值(初始)

这是带有 DbContext Record 实体:

 公共类MyDbContext:DbContext 
{
静态MyDbContext ()
{
Database.SetInitializer< MyDbContext>(null);
}

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

public DbSet< Record> ;记录{get;组; }

受保护的覆盖无效OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove< PluralizingTableNameConvention>();
modelBuilder.Configurations.Add(new Record.Configuration());
}
}

公共类记录
{
public int Id {get;组; }

公共字符串State {get;组; }

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

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

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

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

//从其他上下文插入偷偷摸摸的更新。
使用(var scratchyContext = new MyDbContext())
{
var scratchyRecord = scratchyContext.Records
.Where(r => r.Id == 1&& r.State == Initial)
.Single();

scratchyRecord.State =偷偷摸摸的更新;
scratchyContext.SaveChanges();
}

//尝试更新刚刚由偷偷摸摸的上下文更新并提交的行。
record.State =第二;
context.SaveChanges();
}
}

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

  UPDATE [dbo]。[记录] 
SET [状态] ='Second'
WHERE([Id] = 1)

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



乐观并发控制



让我们调整初始SQL脚本,以在表中包括并发控制列:

 创建表记录(
id int身份不为空的主键,
状态varchar(50)不为null,
并发时间戳记不为空-将此行版本控制列


插入记录(状态)值(初始)

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

 公共类记录
{
public int Id {get;组; }

公共字符串State {get;组; }

//添加此属性。
public byte []并发{组; }

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

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

//添加此配置以告知EF此
//属性/列应用于
//并发检查。
this.Property(t => t.Concurrency)
.IsRowVersion();
}
}
}

现在,如果我们尝试重新运行与先前方案相同的 Main()方法,您会注意到更新语句已生成并执行:

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

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



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

  context.SaveChanges(); 

因此,在这种情况下,如果检查数据库,则会看到<$该行的c $ c> State 值将为'Sneaky Update',因为我们的第二次更新未能通过并发检查。 / p>

最后的想法



如您所见,激活自动乐观方法并不需要做很多事情



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


I have two different processes (on different machines) that are reading and updating a database record.

The rule I need to ensure is that the record must only be updated if the value of it, lets say is "Initial". Also, after the commit I would want to know if it actually got updated from the current process or not (in case if value was other than initial)

Now, the below code performs something like:

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

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

Now couple of questions

1) From looking at the code it appears that after the record is fetched with state "Initial", some other process could have updated it to state "Second" before this process performs SaveChanges. In this case we are unnecessarily overwriting the state to the same value. Is this the case happening here ?

2) If case 1 is not what happens then EntityFramework may be translating the above to something like

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

and performing this as a transaction. This way only one process writes the value. Is this the case with EF default TransactionScope ?

In both cases again how do I know for sure that the update was made from my process as opposed to some other process ?

If this were in-memory objects then in code it would translate to something like assuming multiple threads accessing same data structure

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
       }
}

Thanks

解决方案

In general there are 2 main concurrency patterns that can be used:

  • Pessimistic concurrency: You lock a row to prevent others from unexpectedly changing the data you are currently attempting to update. EF does not provide any native support for this type of concurrency pattern.
  • Optimistic concurrency: Citing from EF's documentation: "Optimistic concurrency involves optimistically attempting to save your entity to the database in the hope that the data there has not changed since the entity was loaded. If it turns out that the data has changed then an exception is thrown and you must resolve the conflict before attempting to save again." This pattern is supported by EF, and can be used rather simply.

Focusing on the optimistic concurrency option, which EF does support, let's compare how your example behaves with and without EF's optimistic concurrency control handling. I'll assume you are using SQL Server.

No concurrency control

Let's start with the following script in the database:

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

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

And here is the code with the DbContext and Record entity:

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();
        }
    }
}

Now, let's test your concurrent update scenario with the following code:

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();
    }
}

If you trace the SQL, you will see that the update statement looks like this:

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

So, in effect, it doesn't care that another transaction sneaked in an update. It just blindly writes over whatever the other update did. And so, the final value of State in the database for that row is 'Second'.

Optimistic concurrency control

Let's adjust our initial SQL script to include a concurrency control column to our table:

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')

Let's also adjust our Record entity class (the DbContext class stays the same):

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();
        }
    }
}

Now, if we try to re-run the same Main() method we used for the previous scenario, you will notice a change in how the update statement is generated and executed:

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

In particular, notice how EF automatically includes the column defined for concurrency control in the where clause of the update statement.

In this case, because there was in fact a concurrent update, EF detects it, and throws a DbUpdateConcurrencyException exception on this line:

context.SaveChanges();

And so, in this case, if you check the database, you'll see that the State value for the row in question will be 'Sneaky Update', because our 2nd update failed to pass the concurrency check.

Final thoughts

As you can see, there isn't much that needs to be done to activate automatic optimistic concurrency control in EF.

Where it gets tricky though is, how do you handle the DbUpdateConcurrencyException exception when it gets thrown? It will largely be up to you to decide what you want to do in this case. But for further guidance on the topic, you'll find more information here: EF - Optimistic Concurrency Patterns.

这篇关于使用实体框架进行原子读写的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆