第二十七节:表达式树、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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。