• 第二十七节:表达式树、EFCore筛选器、乐观并发SQLServer、乐观/悲观并发MySQL


    一. 表达式树

    1. 说明

      我们通常都是写linq表达式,但对于一些动态字段,比如点击列排序,默认是实现不了的,除非手动拼接,非常繁琐,这里就可以通过string类型转换成linq

      官网:https://dynamic-linq.net/    【开源免费】

      支持的方法详见:https://dynamic-linq.net/basic-query-operators  , 不支持orderByDesding

    2. 实操

     (1). 安装程序集 [System.Linq.Dynamic.Core 1.2.19]

     (2). 分别在Where、OrderBy、ThenBy、Select中测试linq的转换

     注:where中写拼接条件支持参数化,默认参数一次为 @0 @1 @2, 依次类推

     (3). 生成的SQL如下图

    代码分析:

    {
        using var db = new EFCore6xDBContext();
    
        var myGender = "女";
        var data = db.UserInfo.Where("userAge=21 && userGender==@0 &&userPwd==@1", myGender, "123456")
                              .OrderBy("addTime desc")
                              .ThenBy("id")
                              .Select("new {userName as myName,userPwd}")
                              .ToDynamicList();
    }

    SQL图:

    二. EFCore筛选器

    1. 说明

      全局查询筛选器:EF Core 会自动将这个查询筛选器应用于涉及到这个实体类型的所有 LINQ 查询

      常用方法:HasQueryFilter、IgnoreQueryFilters

    2. 实操

     (1). 在OnModelCreating方法中进行配置:modelBuilder.Entity<UserInfo>().HasQueryFilter(u => u.delflag == 1);

      protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                //全局筛选器
                modelBuilder.Entity<UserInfo>().HasQueryFilter(u => u.delflag == 1);
          
                modelBuilder.Entity<RoleInfo>(entity =>
                {
                    entity.Property(e => e.id).ValueGeneratedNever();
                });
                OnModelCreatingPartial(modelBuilder);
            }

      注:如果是FluentApi可以在对应实体的配置文件中写即可

     (2). 测试, 一个默认查询,一个忽略筛选器,查看对应SQL语句,如下图

    代码分享:

    {
        using var db = new EFCore6xDBContext();
        //1. 默认执行全局筛选器
        var count1 = db.UserInfo.Count();
    
        //2. 跳过全局筛选器
        var count2 = db.UserInfo.IgnoreQueryFilters().Count();
    
        Console.WriteLine($"总条数为:{count1}");
        Console.WriteLine($"总条数为:{count2}");
    
    }

    SQL生成:

    三. 乐观并发-SQLServer

    1. 原理

      乐观并发的原理:  update GoodsInfo set goodOwner=新值 where id=01 and goodOwner=旧值

      剖析:当Update的时候,如果数据库中的goodOwner值已经被其他操作者更新为其他值了,那么where语句的值就会为false,因此这个Update语句影响的行数就是0,EF Core就知道“发生并发冲突”了, 因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常。

    2. 数据准备

      准备GoodsInfo表,表字段如下图

    3. 单字段

     (1). 设置

        A. FluentApi:  entity.Property(p => p.goodNum).IsConcurrencyToken();

        B. DataAnnotations: 在对应字段上加特性 [ConcurrencyCheck]

     (2). 代码实操

    {
        using var db1 = new EFCore6xDBContext();
        using var db2 = new EFCore6xDBContext();
        try
        {
            var data1 = db1.GoodsInfo.Where(u => u.id == "01").FirstOrDefault();
            var data2 = db2.GoodsInfo.Where(u => u.id == "01").FirstOrDefault();
    
            data1.goodNum -= 2;
            db1.SaveChanges();
    
            data2.goodNum -= 4;
            db2.SaveChanges();
    
            Console.WriteLine("成功了");
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.First();
            var dbValues = await entry.GetDatabaseValuesAsync();
            int newValue = dbValues.GetValue<int>(nameof(GoodsInfo.goodNum));
            Console.WriteLine($"发生并发冲突了,最新值为{newValue}");
        }
    }

    4. 整行数据

     (1). 设置

        A. FluentApi: entity.Property(e => e.rowVersion).IsRowVersion();

        B. DataAnnotations: 在对应字段上加特性[Timestamp]

      另外数据库需要有个单独的字段,可以任意命名,这里我们命名为rowVersion,类型是timeStamp,原理是更改任何一个字段的值,在数据库自动都会让rowVersion这个值发生变化

     (2). 代码实操

       同上

    5. 总结

     (1).乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观并发控制而不是悲观锁。

     (2).如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可;

     (3).如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。

     如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。

     (4). 上述方案仅适用于SQLServer

       A. 在MySQL等数据库中虽然也有类似的timestamp类型,但是由于timestamp类型的精度不够,并不适合在高并发的系统。

       B. 非SQLServer中,可以将并发令牌列的值更新为Guid的值。修改其他属性值的同时,使用h1.RowVer = Guid.NewGuid()手动更新并发令牌属性的值。

    四. 乐观并发-MySQL

    1. 原理

      乐观并发的原理:  update GoodsInfo set goodOwner=新值 where id=01 and goodOwner=旧值

      剖析:当Update的时候,如果数据库中的goodOwner值已经被其他操作者更新为其他值了,那么where语句的值就会为false,因此这个Update语句影响的行数就是0,EF Core就知道“发生并发冲突”了, 因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常

    2. 数据准备

      准备GoodsInfo表,表字段如下图

    3. 单字段

     (1). 设置

        A. FluentApi:  entity.Property(p => p.goodNum).IsConcurrencyToken();

        B. DataAnnotations: 在对应字段上加特性 [ConcurrencyCheck]

     (2). 代码实操

        经测试,都是是正常的!!!!

    {
        using var db1 = new EFCore6xDBContext();
        using var db2 = new EFCore6xDBContext();
        try
        {
            var data1 = db1.GoodsInfo.Where(u => u.id == "01").FirstOrDefault();
            var data2 = db2.GoodsInfo.Where(u => u.id == "01").FirstOrDefault();
    
            data1.goodNum -= 2;
            db1.SaveChanges();
    
            data2.goodNum -= 4;
            db2.SaveChanges();
    
            Console.WriteLine("成功了");
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.First();
            var dbValues = await entry.GetDatabaseValuesAsync();
            int newValue = dbValues.GetValue<int>(nameof(GoodsInfo.goodNum));
            Console.WriteLine($"发生并发冲突了,最新值为{newValue}");
        }
    }

    4. 整行数据

     (1). 设置

        A. FluentApi: entity.Property(e => e.rowVersion).IsRowVersion();

        B. DataAnnotations: 在对应字段上加特性[Timestamp]

        C. 每次修改任意字段,都手动给改一下rowVersion的值

     (2). 代码实操

       经测试,两种配置都无效!!!

    {
        using var db1 = new EFCore6xDBContext();
        using var db2 = new EFCore6xDBContext();
    
        try
        {
            var data1 = db1.GoodsInfo.Where(u => u.id == "01").FirstOrDefault();
            var data2 = db2.GoodsInfo.Where(u => u.id == "01").FirstOrDefault();
    
            data1.goodNum -= 2;
            data1.rowVersion = Guid.NewGuid().ToString("N");
            db1.SaveChanges();
    
            data2.goodNum -= 4;
            data2.rowVersion = Guid.NewGuid().ToString("N");
            db2.SaveChanges();
    
            Console.WriteLine("成功了");
    
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.First();
            var dbValues = await entry.GetDatabaseValuesAsync();
            int newValue = dbValues.GetValue<int>(nameof(GoodsInfo.goodNum));
            Console.WriteLine($"发生并发冲突了,最新值为{newValue}");
    
        }

    五. 悲观并发-MySQL

    1. 说明

      EFCore默认不支持悲观并发,需要开发人员编写原生SQL语句来使用悲观并发控制,不同数据库的语法不一样, 下面以MySQL为例

      悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。

    2. 实操

      MySQL方案: select * from T_Houses where Id=1 for update

      如果有其他的查询操作也使用for update来查询Id=1的这条数据的话,那些查询就会被挂起,一直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继续执行。

      如下代码:第一个用户执行到 await Task.Delay 等待的时候, 另外的用户进来则卡在db1.GoodsInfo.FromSqlInterpolated这里,只有第一个用户savechange释放锁后,其它用户才能仅需执行

    代码分享:

    {
        Console.WriteLine("请输入您的姓名");
        string name = Console.ReadLine();
        using var db1 = new EFCore6xDBContext();
        using var tx = await db1.Database.BeginTransactionAsync();
        Console.WriteLine("准备Select " + DateTime.Now.TimeOfDay);
        var goodInfo = await db1.GoodsInfo.FromSqlInterpolated($"select * from GoodsInfo where id=01 for update").SingleAsync();
        Console.WriteLine("完成Select " + DateTime.Now.TimeOfDay);
        if (string.IsNullOrEmpty(goodInfo.goodOwner))
        {
            await Task.Delay(20000);
            goodInfo.goodOwner = name;
            await db1.SaveChangesAsync();   //针对上面的for update 解锁
            Console.WriteLine("抢到手了");
        }
        else
        {
            if (goodInfo.goodOwner == name)
            {
                Console.WriteLine("这件物品已经是你的了,不用抢");
            }
            else
            {
                Console.WriteLine($"这件物品已经被{goodInfo.goodOwner}抢走了");
            }
        }
        await tx.CommitAsync();
        Console.ReadKey();
    
    }

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    SignalR了解
    轮询、长轮询、长连接、socket连接、WebSocket
    WebSocket
    FileSaver.js 实现浏览器文件导出
    上传文件调用webapi方式
    JS离开页面 弹窗
    微信公众号开发 VS2015本地调试
    C# 微信 企业号通知消息
    nginx防止DDOS攻击配置
    如何在终端使用后台运行模式启动一个Linux应用程序
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/16411625.html
Copyright © 2020-2023  润新知