《ASP.NET Core技术内幕与项目实战》精简集-EFCore2.7:杂项(查询筛选器、原生SQL、并发、状态跟踪等)
本节内容,涉及4.5(P96-P97)、5.2(P131-P141)。主要NuGet包:如前章节所述
一、查看SQL语句,调试LINQ语句
//在DbContext类的OnConfiguring方式中,增加显示SQL的配置。即可在控制台中查询生成的SQL public class MyDbContext: DbContext { ...... protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { ...... optionsBuilder.LogTo(Console.WriteLine); } ...... } //EFCore可以对接多种数据库,所以同样的LINQ,生成的SQL并不相同 //EFCore相当于一个机翻器,将LINQ翻译成不同数据库的SQL //因为是机翻,所以翻译成的SQL,可能存在性能问题,所以需要调整LINQ //LINQ非常灵活,同样的结果,会有多种表达方式,如果碰到性能问题,就需要进行调试 //通过optionsBuilder.LogTo可以简单的输出SQL,从而进行调试 //如果是复杂的Linq,推荐使用专业工具LinqPad进行编写和调试
二、查询筛选器,实现软删除和多租户等功能
//我们为Book实体类增加一个软删除属性 public bool IsDeleted { get; set; } //在Book的映射配置FluentApi中设置全局筛选器 modelBuilder.Entity<Book>(b => { ...... b.HasQueryFilter(b => b.IsDeleted == false); }); //通过以上两部设置,所有对Book实体的查询,都会在Where查询中,自动增加IsDelete == false的查询 //查询时,可以设置忽略筛选器 ctx.Books.IgnoreQueryFilters().Where(b => b.Id > 0);
三、EFCore框架自带的执行原生SQL的方法(实际项目中,一般使用Dapper执行原生SQL,特别是做一些复杂报表)
//执行SQL非查询语句 //内插值方式传入参数,参数为FomattableString类型,不会有SQL注入问题 //定义最低价格,作为内插值参数 var lowPrice = 35.0; //ExecuteSqlInterpolatedAsync方法,返回插入数据库的总行数 int rows = await ctx.Database.ExecuteSqlInterpolatedAsync(@$" insert into T_Books(Title, PubTime, Price, AuthorName) select Title, PubTime, Price, Name from T_Books where Price>{lowPrice}" "); //执行SQL查询语句 //books为IQueryable,可以利用延迟执行和复用等特性 //FromSqlInterpolated方法只能查询单表,且属性和列要对应 //可以在返回的集合中,继续使用Include、Select等方向,进一步查询 int year = 2000; var books = ctx.Books.FromSqlInterpolated(@$" select * from T_Books where DataPart(year, PubTime)>{year} order by newid() "); foreach(var item in books.Skip(3).Take(6)) { Console.WriteLine(b.Title); }
四、悲观并发和乐观并发
1、悲观并发一般采用数据库的表锁和行锁,实际开发中较少使用,此略过。但书中有个知识点,补充如下:
//锁需要结合事务使用 using var ctx = new MyDbContext(); using var tx = await ctx.Database.BeginTransactionAsync(); //启动事务 var h1 = await ctx.Houses.FromSqlInterpolated($@" select * from T_Houses where Id = 1 for update //for update加锁 "); ...... await tx.CommitAsync(); //提交事务
2、乐观并发,实际开发中一般使用乐观并发。基本原理是设置一个并发令牌列,更新时,曾加一个旧值的Where查询条件,如Update T_Houses set Owner = 新值 where Id =1 and Owner = 旧值。假设现在有两个并发更新,它们同时取出旧值,并发1快一点,先执行,此时Owner等于旧值,更新成功;而并发2慢了一步,此时Owner 已经被并发1更新,不等于旧值,更新失败。乐观并发的设置有两种方式,一种是直接将更新列作为并发令牌列,另一种是新建一个timestamp类型的列作为并发令牌列(也叫rowversion列),每次插入或更新时,数据库会自动生成列值(MySQL目前不支持自动生成,可以手动生成一个Guid值)。
①方式一:将需要更新的属性Owner,设置为并发令牌列。从异常对象中获取当前数据库值有一些难度,先了解。
//实体类,属性Owner为房子的拥有者,如果有值,则说明房子被占 //House.cs public class House { public long Id { get; set; } public string Name { get; set; } public string? Owner { get; set; } } //DbContext类 //配置映射时,设置Owner属性列为并发令牌列,b.Property(h => h.Owner).IsConcurrencyToken(); public class MyDbContext: DbContext { public DbSet<House> Houses { get; set; } ...... protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<House>(b => { ...... b.Property(h => h.Owner).IsConcurrencyToken(); }); } } //模拟一个抢房子过程,生成编译后,在Debug目录,同时打开两个控制台程序进行测试 using var ctx = new MyDbContext(); /*新增几个房间 var h1 = new House { Name = "Room101" }; var h2 = new House { Name = "Room201" }; var h3 = new House { Name = "Room301" }; ctx.AddRange(h1, h2, h3); await ctx.SaveChangesAsync(); */ //模拟抢房子 Console.WriteLine("请输入您的姓名"); string ownName = Console.ReadLine(); var h1 = await ctx.Houses.SingleAsync(h => h.Name == "Room101"); //如果无房主 if (string.IsNullOrEmpty(h1.Owner)) { await Task.Delay(5000); h1.Owner = ownName; try { await ctx.SaveChangesAsync(); Console.WriteLine("抢到手了"); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.First(); var dbValues = await entry.GetDatabaseValuesAsync(); string newOwner = dbValues.GetValue<string>(nameof(House.Owner)); Console.WriteLine($"并发冲突,被{newOwner}提前抢走了"); } } //如果有房主 else { if (h1.Owner == ownName) { Console.WriteLine("房间已经是你的了,不用抢了"); } else { Console.WriteLine($"房子已经被 {h1.Owner} 抢走了!"); } } Console.ReadLine();
②新增一个rowversion类型的并发令牌列
//首先,实体增加一个byte[]类型的列 public class House { public long Id { get; set; } public string Name { get; set; } public string? Owner { get; set; } public byte[] RowVersion { get; set; } } //其次,在DbContext的映射配置中,配置这个属性列为IsRowVersion b.Property(h => h.RowVersion).IsRowVersion(); //乐观并发的配置,都非常简单。只有原理和模拟实现,有一些复杂
五、状态跟踪。这部分留到《EFCore7的重磅更新-批量操作》章节来学习