要专业系统地学习EF推荐《你必须掌握的Entity Framework 6.x与Core 2.0》。这本书作者(汪鹏,Jeffcky)的博客:https://www.cnblogs.com/CreateMyself/
这里学到并发冲突的高级解析,就是基于前面的内容,我们来封装一个比较完整的解决方案。
这次主要get到这些知识点,有新的,也有对之前一些知识点的刷新
1、在并发冲突(一)随笔中我开头举的例子不恰当
2、打破了必须要配置并发才能捕获并发冲突,之前怎么就没想到并发删除呢?
3、在并发冲突(二)中我们看到EF中有个枚举认识到客户端获胜和数据库获胜这些东西确实出自官方,并且我说到这个没有必要用到。在高级版这里,作者将他扩展了
4、tracking.Reload()方法的认识
当实体被删除时,重新加载,设置追踪状态为Detached
当实体被更新时,重新加载,设置追踪状态为Unchanged
5、when对我来说是一个新关键字,是的,我之前不知道C#中还有这个关键字。它可以配合try/catch、switch/case来使用。详细学习参考MSDN:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/when
6、nameof()对我来说也是一个新的方法,以前碰到过这样的问题,不过当时是在JS中。就是一个变量str,我怎么得到“str”字符串?那么就用nameof.Console.WriteLine(nameof(str)); // result:str
7、客户端数据库合并获胜还是按照我自己的来的 ,和作者的代码不一样
并发冲突(一)中我举了这样一个例子
using (EFDbContext ctx1 = new EFDbContext()) using (EFDbContext ctx2 = new EFDbContext()) { var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "李四"; stu1.Score = 99; stu2.Name = "张三"; stu2.Score = 100; try { ctx2.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { throw ex; } }
虽然确实是并发进来,但是因为数据的问题又不会造成并发冲突异常。因为第二次修改的值和查询出来的实体值是一样的。EF认为没有做修改。
因为数据是从数据库中拿过来的,不能保证内存中的值和数据库一致。这里就纪念一下吧,我对这个问题没有去想处理办法,我觉得不用去处理。
前面的一直肯定这一点:想要捕获并发冲突就一定要配置,但其实如果是并发删除进来就不必了。只不过这种情况我们平时不会这么想方设法地去写。
下面就来对并发冲突慢慢封装一个完整的解决方案出来吧。
还是按照从简单的到复杂的来
这是一个简单的Student类,使用rowversion的方式配置并发
public class BaseEntity { public BaseEntity() { this.Id = Guid.NewGuid().ToString(); this.AddTime = DateTime.Now; } public string Id { get; set; } public DateTime AddTime { get; set; } } public class Student:BaseEntity { public string Name { get; set; } public int Score { get; set; } public byte[] RowVersion { get; set; } }
简单的重试策略,扩展DBcontext类
简单重试策略 public static partial class DbContextExtensions { public static int SaveChanges(this DbContext context, Action<IEnumerable<DbEntityEntry>> handler, int retryCount = 3) { if (retryCount < 0) { throw new ArgumentOutOfRangeException(nameof(retryCount),"不能小于0"); } for (int retry = 0; retry < retryCount; retry++) { try { return context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { handler(ex.Entries); } } return context.SaveChanges(); } }
现在来调用就简洁了很多
using (EFDbContext ctx1 = new EFDbContext()) using (EFDbContext ctx2 = new EFDbContext()) { var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "李四"; stu1.Score = 99; stu2.Name = "王五"; stu2.Score = 88; ctx1.SaveChanges(); ctx2.SaveChanges(entites => { var tracking = entites.Single(); tracking.OriginalValues.SetValues(tracking.GetDatabaseValues()); }); }
现在来用Polly库,更简洁
Polly使用分三步
1、指定要捕获的异常类型
2、指定异常处理的策略:重试、断路、超时……
3、委托一个给polly可能造成该异常的方法,让polly去执行
public static partial class DbContextExtensions { public static int SaveChanges(this DbContext context, Action<IEnumerable<DbEntityEntry>> handle, int retryCount = 3) { var retryPolicy = Policy.Handle<DbUpdateConcurrencyException>() .Retry(retryCount, (exception, count) => { handle(((DbUpdateConcurrencyException)exception).Entries); }); return retryPolicy.Execute(context.SaveChanges); } }
到现在我们还没有加入客户端获胜、数据库获胜、合并获胜这些情况的处理
我们想到的是不是应该封装一个方法供外部去调用,只需要告诉我谁获胜就行了?
先来一个枚举
public enum RefreshReflict { ClientWins =10, StoreWins = 20, MergeClientAndStore = 30 }
这里就说到Refresh了,作者对RefreshEFState类进行了如下的扩展,在Refresh中分别去实现客户端获胜这些……
public static class RefreshEFStateExtension { public static DbEntityEntry Refresh(this DbEntityEntry tracking, RefreshReflict refreshMode) { switch (refreshMode) { case RefreshReflict.ClientWins: break; case RefreshReflict.StoreWins: break; case RefreshReflict.MergeClientAndStore: break; default: break; } return tracking; }
最后再次对Dbcontext进行扩展,提供一个最终供外部调用的SaveChanges方法
public static partial class DbContextExtensions { public static int SaveChanges(this DbContext context, RefreshReflict refreshMode, int retryCount = 3) { if (retryCount < 0) { throw new ArgumentOutOfRangeException(nameof(retryCount),"不能小于0"); } return context.SaveChanges(entities => { entities.ToList().ForEach(tracking => tracking.Refresh(refreshMode)); }, retryCount); } }
基本就是这样。最后因为我和作者对合并获胜写的不一样,贴出来看下
我按照自己的写法来需要采用DataAnnotations的方式来配置并发属性,所以直到现在我都不认为我的是对的,但按照作者的写法来确实也出不来结果,还是没有真正理解?
public class Teacher:BaseEntity { [ConcurrencyCheck] public string Name { get; set; } public int Score { get; set; } [ConcurrencyCheck] public string Subject { get; set;} }
public static class RefreshEFStateExtensions { public static DbEntityEntry Refresh(this DbEntityEntry tracking, RefreshReflict refreshMode) { switch (refreshMode) { case RefreshReflict.ClientWins: { Console.WriteLine("ClientWins"); var databaseValues = tracking.GetDatabaseValues(); if (databaseValues == null) { tracking.State = EntityState.Deleted; } else { tracking.OriginalValues.SetValues(tracking.GetDatabaseValues()); } break; } case RefreshReflict.StoreWins: { Console.WriteLine("StoreWins"); // 当实体被删除时,重新加载,设置追踪状态为Detached // 当实体被更新时,重新加载,设置追踪状态为Unchanged // 这里对并发更新,只是简单的忽略,如果有其他的操作自行补吧 tracking.Reload(); } break; case RefreshReflict.MergeClientAndStore: { Console.WriteLine("MergeClientAndStore"); var databaseValues = tracking.GetDatabaseValues(); if (databaseValues == null) { tracking.State = EntityState.Deleted; } else { var originalValues = tracking.OriginalValues; var currentValues = tracking.CurrentValues; originalValues.SetValues(databaseValues); object obj = tracking.Entity; List<string> ConcurrencyProperties = new List<string>(); obj.GetType().GetProperties().ToList().ForEach(property => { if (Attribute.IsDefined(property, typeof(ConcurrencyCheckAttribute))) { ConcurrencyProperties.Add(property.Name); } }); // ConcurrencyProperties.ForEach(property => Console.WriteLine(property)); // Name Subject foreach (var item in ConcurrencyProperties) { tracking.Property(item).IsModified = false; } } // 作者是这样写的 //var originalValues = tracking.OriginalValues.Clone(); //tracking.OriginalValues.SetValues(databaseValues); //databaseValues.PropertyNames.Where(property => !object.Equals(originalValues[property], databaseValues[property])) // .ToList() // .ForEach(property => tracking.Property(property).IsModified = false); } break; } return tracking; } }
EF中的并发内容就到这里结束了,后面的就是性能优化和实战了,继续学吧。