《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的重磅更新-批量操作》章节来学习

posted @ 2022-10-29 20:32  functionMC  阅读(352)  评论(0编辑  收藏  举报