什么是并发?
并发分悲观并发和乐观并发。
悲观并发:比如有两个用户A,B,同时登录系统修改一个文档,如果A先进入修改,则系统会把该文档锁住,B就没办法打开了,只有等A修改完,完全退出的时候B才能进入修改。
乐观并发:同上面的例子,A,B两个用户同时登录,如果A先进入修改紧跟着B也进入了。A修改文档的同时B也在修改。如果在A保存之后B再保存他的修改,此时系统检测到数据库中文档记录与B刚进入时不一致,B保存时会抛出异常,修改失败。
EF中如何控制并发?
Entity Framework不支持悲观并发,只支持乐观并发。
如果要对某一个表做并发处理,就在该表中加一条Timestamp类型的字段。注意,一张表中只能有一个Timestamp的字段。
Data Annotations中用Timestamp来标识设置并发控制字段,标识为Timestamp的字段必需为byte[]类型。
public class Person { public int PersonId { get; set; } public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [Timestamp] public byte[] RowVersion { get; set; } }
Fluent API用IsRowVersion方法
modelBuilder.Entity<Person>().Property(p => p.RowVersion).IsRowVersion();
我们看到生成的数据库中,RowVersion是timestamp类型。
下面我们写一段代码来测试一下:
static void Main(string[] args) { var person = new Person { FirstName = "Rowan", LastName = "Miller", SocialSecurityNumber = 12345678 }; //新增一条记录,保存到数据库中 using (var con = new BreakAwayContext()) { con.People.Add(person); con.SaveChanges(); } var firContext = new BreakAwayContext(); //取第一条记录,并修改一个字段:这里是修改了FirstName //先不保存 var p1 = firContext.People.FirstOrDefault(); p1.FirstName = "Steven"; //再创建一个Context,同样取第一条记录,修改LastName字段并保存 using (var secContext = new BreakAwayContext()) { var p2 = secContext.People.FirstOrDefault(); p2.LastName = "Francis"; secContext.SaveChanges(); } try { firContext.SaveChanges(); Console.WriteLine(" 保存成功"); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine(ex.Entries.First().Entity.GetType().Name + " 保存失败"); } Console.Read(); }
上面我们实例化了三个DbContext,第一个增加一条记录到数据库中,第二个修改刚增加的记录但不保存,然后第三个Context也取刚新增的记录并保存,最后再保存第二个Context,结果保存失败。
可以看到我们的并发控制取到了作用。
分析EF生成的SQL语句:
exec sp_executesql N'update [dbo].[People] set [LastName] = @0 where (([PersonId] = @1) and ([RowVersion] = @2)) select [RowVersion] from [dbo].[People] where @@ROWCOUNT > 0 and [PersonId] = @1',N'@0 nvarchar(max) ,@1 int,@2 binary(8)',@0=N'Francis',@1=1,@2=0x00000000000007D1
可以看到,它在取对应记录的时候把RowVersion也作为筛选条件。上面例子中的secContext保存的时候,数据库中的RowVersion字段的值就变了,所以firContext保存的时候用原来的RowVersion取值,自然就取不到相应的记录而报错。
如果我们只是要对某个字段作并发控制呢?别着急,EF也有办法。
Data Annotations中用ConcurrencyCheck来标识
public class Person { public int PersonId { get; set; } [ConcurrencyCheck] public int SocialSecurityNumber { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public byte[] RowVersion { get; set; } }
Fluent API用IsConcurrencyToken方法
modelBuilder.Entity<Person>().Property(p => p.SocialSecurityNumber).IsConcurrencyToken();
上面的实体中,我们将SocialSecurityNumber(社会保险号)标识为开放式并发,也写一个类似的代码测试一下:
static void Main(string[] args) { var person = new Person { FirstName = "Rowan", LastName = "Miller", SocialSecurityNumber = 12345678 }; //新增一条记录,保存到数据库中 using (var con = new BreakAwayContext()) { con.People.Add(person); con.SaveChanges(); } var firContext = new BreakAwayContext(); //取第一条记录,并修改SocialSecurityNumber字段 //先不保存 var p1 = firContext.People.FirstOrDefault(); p1.SocialSecurityNumber = 123; //再创建一个Context,同样取第一条记录, //修改SocialSecurityNumber字段并保存 using (var secContext = new BreakAwayContext()) { var p2 = secContext.People.FirstOrDefault(); p2.SocialSecurityNumber = 456; secContext.SaveChanges(); } try { firContext.SaveChanges(); Console.WriteLine(" 保存成功"); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine(ex.Entries.First().Entity.GetType().Name + " 保存失败"); } Console.Read(); }
运行结果同样是保存失败,说明我们的并发控制起作用了。
分析一下EF执行的SQL:
exec sp_executesql N'update [dbo].[People] set [SocialSecurityNumber] = @0 where (([PersonId] = @1) and ([SocialSecurityNumber] = @2)) ',N'@0 int,@1 int,@2 int',@0=123,@1=1,@2=12345678
可以看到,EF将我们要并发控制的列SocialSecurityNumber也作为一个筛选条件,这样firContext保存的时候也会因为的数据库中SocialSecurityNumber值变了,取不到对应的记录而更新失败。
补充一下:如果是EDMX如何将字段设置为Concurrency。很简单,在对应的字段上右键-属性。在打开的属性窗口中有一个并发模式,你将它选择为Fixed即可。