第二十七节:表达式树、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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2022-06-25 15:06  Yaopengfei  阅读(425)  评论(1编辑  收藏  举报