EF6学习笔记二十九:并发冲突(三)

要专业系统地学习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;}
    }
View Code
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;
        }
    }
View Code

EF中的并发内容就到这里结束了,后面的就是性能优化和实战了,继续学吧。

 

posted @ 2019-02-20 22:16  张四海  阅读(415)  评论(0编辑  收藏  举报