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.RowVer).IsRowVersion(); }