我有两个不同的进程(在不同的机器上)正在读取和更新数据库记录。
我需要确保的规则是,只有记录的值才能更新记录,例如" 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
}
}
由于
答案 0 :(得分:13)
通常,可以使用两种主要的并发模式:
关注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')
以下是包含DbContext
和Record
实体的代码:
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。