EF Core优化之AsNoTracking

EF Core默认会对通过上下文查询出来的所有实体类进行跟踪,以便于在执行SaveChanges的时候把实体类的改变同步到数据库中。上下文不仅会跟踪对象的状态改变,还会通过快照的方式记录实体类的原始值,这是比较消耗资源的。
因此,如果开发人员能够确认通过上下文查询出来的对象只是用来展示,不会发生状态改变,那么可以使用AsNoTracking方法告诉IQueryable在查询的时候“禁用跟踪”。

1
2
3
4
5
Blog[] blogs = ctx.Blogs.AsNoTracking().Take(3).ToArray();
Blog b1 = blogs[0];
b1.Id = 100;
EntityEntry entry1=ctx.Entry(b1);
Console.WriteLine(entry1.State);

上面代码的执行结果是“Detached”,也就说使用AsNoTracking查询出来的实体类是不被上下文跟踪的。
因此,在项目开发中,如果我们查询出来的对象不会被修改、删除等,那么在查询的时候,可以启用AsNoTracking,这样就能降低EF Core的资源占用。

EF Core中批量删除和更新数据

删除 Age 大于 10 的数据

1
2
await using var db = new MyDbContext();
await db.MyEntities.Where(static x => x.Age > 10).ExecuteDeleteAsync();

所有员工的工资增加 1000

1
2
await context.Employees.ExecuteUpdateAsync
(s => s.SetProperty(e => e.Salary, e => e.Salary + 1000));

全局查询筛选器

EF Core支持在配置实体类的时候,为实体类设置全局查询筛选器,EF Core会自动将全局查询筛选器应用于涉及这个实体类型的所有LINQ查询。这个功能常见的应用场景有“软删除”和“多租户”。

我们可以给对应实体类设置一个全局筛选器,这样所有的查询都会自动增加全局查询筛器,被软删除的数据就会自动从查询结果中过滤掉。

首先,我们给实体类增加一个bool类型的属性IsDeleted,如果对应的数据被标记为已删除,那么IsDeleted的值就会true,否则就是false。
在实体类的Fluent API中增加一句代码:

1
2
3
4
5
6
7
8
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().ToTable("Blog")
.HasQueryFilter(b => b.IsDeleted == false)//全局筛选器
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId);
}

悲观并发控制

EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。不同数据库的语法不一样。
MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Console.WriteLine("请输入您的姓名");
string name = Console.ReadLine();
using MyDbContext ctx = new MyDbContext();
using var tx = await ctx.Database.BeginTransactionAsync();//事务
Console.WriteLine("准备Select " + DateTime.Now.TimeOfDay);
var h1 = await ctx.Houses.FromSqlInterpolated($"select * from T_Houses where Id=1 for update")
.SingleAsync();
Console.WriteLine("完成Select " + DateTime.Now.TimeOfDay);
if (string.IsNullOrEmpty(h1.Owner))
{
await Task.Delay(5000);
h1.Owner = name;
await ctx.SaveChangesAsync();
Console.WriteLine("抢到手了");
}
else
{
if (h1.Owner == name)
{
Console.WriteLine("这个房子已经是你的了,不用抢");
}
else
{
Console.WriteLine($"这个房子已经被{h1.Owner}抢走了");
}
}
await tx.CommitAsync();
Console.ReadKey();

乐观并发控制

两种方案:并发令牌、RowVersion
如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可;
如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。

乐观并发控制的原理

Update T_Houses set Owner=新值
where Id=i and Owner=旧值

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

1.把被并发修改的属性使用IsConcurrencyToken()设置为并发令牌。

1
2
modelBuilder.Entity<Blog>().ToTable("Blog")
.Property(e => e.Owner).IsConcurrencyToken();

MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Console.WriteLine("请输入您的姓名");
string name = Console.ReadLine();
using MyDbConfigText ctx = new MyDbConfigText() ;
var h1 = await ctx.Blogs.SingleAsync(h => h.Id == 1);
if (string.IsNullOrEmpty(h1.Owner))
{
await Task.Delay(5000);
h1.Owner = name;
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(Blog.Owner));
Console.WriteLine($"并发冲突,被{newOwner}提前抢走了");
}
}
else
{
if (h1.Owner == name)
{
Console.WriteLine("这个房子已经是你的了,不用抢");
}
else
{
Console.WriteLine($"这个房子已经被{h1.Owner}抢走了");
}
}
Console.ReadLine();

SQLServer的ROWVERSION

SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对于ROWVERSION类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列其生成新值。
在SQLServer中,timestamp和rowversion是同一种类型的不同别名而已。

我们先定义一个byte[]类型属性的House类

1
2
3
4
5
6
7
class House
{
public long Id { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
public byte[] RowVer { get; set; }
}

对House实体类进行配置,对RowVer属性设置IsRowVersion。

1
2
3
4
5
6
7
public void Configure(EntityTypeBuilder<House> builder)
{
builder.ToTable("T_Houses");
builder.Property(h => h.Name).IsRequired();
//builder.Property(h=>h.Owner).IsConcurrencyToken();
builder.Property(h => h.RowVer).IsRowVersion();
}