02020506 EF Core高级06-EF Core批量删除&更新&插入、全局筛选器、软删除、全局筛选的性能问题
1. EF Core如何批量删除、更新、插入(视频3-36)
1.1 EF Core中插入数据(单条)
1、EF Core中不支持高效的删除、更新、插入数据,都是逐条操作。AddRange、DeleteRange等。
2、理想的:Delete from T_Books where Price>33
3、看看ctx.RemoveRange(ctx.Books.Where(b=> b.Price > 33))、 AddRange、批量更新等内部是怎么实现的。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// EF Core插入数据形式1
using (MyDbContext ctx = new MyDbContext())
{Article a01 = new Article { Message = "abc", Price = 10, Title = "ABC" };Article a02 = new Article { Message = "abc", Price = 10, Title = "ABC" };Article a03 = new Article { Message = "abc", Price = 10, Title = "ABC" };ctx.Articles.Add(a01);ctx.Articles.Add(a02);ctx.Articles.Add(a03);
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// EF Core插入数据形式2
using (MyDbContext ctx = new MyDbContext())
{Article a01 = new Article { Message = "abc", Price = 10, Title = "ABC" };Article a02 = new Article { Message = "abc", Price = 10, Title = "ABC" };Article a03 = new Article { Message = "abc", Price = 10, Title = "ABC" };ctx.Articles.AddRange(a01, a02, a03);
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// EF Core插入数据形式3
using (MyDbContext ctx = new MyDbContext())
{Article a01 = new Article { Message = "abc", Price = 10, Title = "ABC" };Article a02 = new Article { Message = "abc", Price = 10, Title = "ABC" };Article a03 = new Article { Message = "abc", Price = 10, Title = "ABC" };ctx.AddRange(a01, a02, a03);
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:
1. 上述三种写法在EF Core中对数据库的操作是一样的,但这还是属于EF Core中数据一条条的插入。
2. AddRange方法知识用了for循环一条条的插入,也并不是一次性插入多条数据。
3. 在数据库中还是执行了三条insert语句。如果插入一万条呢?那么会有一万条insert,这样会造成性能损失。
1.2 EF Core中删除数据(单条)
// EF Core删除数据形式1
using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;namespace OneToMany
{class Program{static async Task Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item in ctx.Articles.Where(a => a.Id > 20)){ctx.Remove(item);}ctx.SaveChanges();}Console.WriteLine();}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using Dapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System;
using System.Data.Common;
using System.Linq;
using System.Threading.Tasks;namespace OneToMany
{class Program{static async Task Main(string[] args){using (MyDbContext ctx = new MyDbContext()){ctx.RemoveRange(ctx.Articles.Where(a => a.Id > 1));ctx.SaveChanges();}Console.WriteLine();}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:上述两种写法在EF Core中对数据库的操作是一样的,但这还是属于EF Core中数据一条条的删除。
2. 为啥不用原生SQL实现
2.1 使用原生SQL的缺点
1、原生SQL语句需要把表名、列名等硬编码到SQL语句中,不符合模型驱动、分层隔离等思想,程序员直接面对数据库表,无法利用EF Core强类型的特性,如果模型发生改变,必须手动变更SQL语句。
1.1 如update t set Price = Price + 1
1.2 如delete from t where Id > 10
2、无法利用EF Core强大的SQL翻译机制来屏蔽不同底层数据库的差异。
3、EF Core官方迟迟未支持的原因。
2.2 微软官方关于此处的探讨
- https://github.com/dotnet/efcore/issues/795
2.3 老师的开源实现
1、Zack.EFCore.Batch
2、await ctx.DeleteRangeAsync<Book>(b => b.Price > n || b.AuthorName =="zack yang");
await ctx.BatchUpdate<Book>().Set(b => b.Price, b => b.Price + 3).Set(b => b.Title, b => s).Set(b =>b.AuthorName,b=>b.Title.Substring(3,2)+b.AuthorName.ToUpper()).Set(b => b.PubTime, b => DateTime.Now).Where(b => b.Id > n || b.AuthorName.StartsWith("Zack")).ExecuteAsync();
ctx.BulkInsert(books);
2.4 Zack.EFCore.Batch包使用
- 以Zack.EFCore.Batch -version 1.4.6示例
- 官方文档:https://github.com/yangzhongke/Zack.EFCore.Batch
3. EF Core全局查询筛选器(视频3-37)
3.1 全局查询筛选器概述
1、全局查询筛选器:EF Core 会自动将这个查询筛选器应用于涉及这个实体类型的所有 LINQ 查询。
2、场景:软删除、多租户。
3.2 什么是软删除

delete from t where Name = "张三" // 直接从数据库删除,无法恢复,属于硬删除。增加IsDeleted列,为true表示屏蔽,为false表示未屏蔽,这样可以追溯。
3.3 软删除的用法示例一
- 在在02020503章4.5节基础上继续
1、builder.HasQueryFilter(b=>b.IsDeleted==false);
2、测试一下如下的代码,查看生成的SQL:
ctx.Books.Where(b=>b.Price>20).ToArray()
3、忽略:ctx.Books.IgnoreQueryFilters().Where(b => b.Title.Contains("o")).ToArray()
查看SQL
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// Article.cs
using System.Collections.Generic;namespace OneToMany
{class Article // 文章{public long Id { get; set; }public string Title { get; set; }public string Message { get; set; }public List<Comment> Comments { get; set; } = new List<Comment>();public int Price { get; set; }public bool IsDeleted { get; set; } // 增加状态列}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 迁移数据库之前
Id Title Message Price
1 杨中科入选中科院 大新闻 30
577 ABC abc 10
578 ABC abc 10
579 ABC abc 10
580 ABC abc 10
581 ABC abc 10
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 迁移数据库
PM> add-migration isdeleted
Build started...
Build succeeded.
The Entity Framework tools version '5.0.4' is older than that of the runtime '5.0.5'. Update the tools for the latest features and bug fixes.
To undo this action, use Remove-Migration.
PM> update-database
Build started...
Build succeeded.
The Entity Framework tools version '5.0.4' is older than that of the runtime '5.0.5'. Update the tools for the latest features and bug fixes.
Applying migration '20251006023435_isdeleted'.
Done.
PM>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 迁移数据库之后
Id Title Message Price IsDeleted
1 杨中科入选中科院 大新闻 30 0
577 ABC abc 10 0
578 ABC abc 10 0
579 ABC abc 10 0
580 ABC abc 10 0
581 ABC abc 10 0
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Threading.Tasks;namespace OneToMany
{class Program{static async Task Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item in ctx.Articles){Console.WriteLine(item.Id + item.Price);}}Console.WriteLine();}}
}控制台输出:
31
587
588
589
590
591
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Linq;
using System.Threading.Tasks;namespace OneToMany
{class Program{static async Task Main(string[] args){using (MyDbContext ctx = new MyDbContext()){var a = ctx.Articles.Single(a => a.Id == 1);a.IsDeleted = true;ctx.SaveChanges();}Console.WriteLine();}}
}// 此时数据库状态:
Id Title Message Price IsDeleted
1 杨中科入选中科院 大新闻 30 1 // 状态已经更新
577 ABC abc 10 0
578 ABC abc 10 0
579 ABC abc 10 0
580 ABC abc 10 0
581 ABC abc 10 0
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
using System;
using System.Linq;
using System.Threading.Tasks;namespace OneToMany
{class Program{static async Task Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item in ctx.Articles) // 查询所有{Console.WriteLine(item.Id + item.Price);}Console.WriteLine("********************");foreach (var item in ctx.Articles.Where(a => a.IsDeleted == false)) // 依据IsDeleted来查询{Console.WriteLine(item.Id + item.Price);}}Console.WriteLine();}}
}控制台输出:
31
587
588
589
590
591
********************
587
588
589
590
591// SQL语句1
SELECT [t].[Id], [t].[IsDeleted], [t].[Message], [t].[Price], [t].[Title]FROM [T_Articles] AS [t]********************
// SQL语句2
SELECT t.Id, t.IsDeleted, t.Message, t.Price, t.TitleFROM T_Articles AS tWHERE t.IsDeleted == CAST(0 AS bit)),说明:SQL语句2增加了IsDeleted过滤
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
总结:每一次都要使用IsDeleted比较麻烦,可以使用全局筛选器来实现。
3.4 软删除的用法示例二
// ArticleConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace OneToMany
{class ArticleConfig : IEntityTypeConfiguration<Article>{public void Configure(EntityTypeBuilder<Article> builder){builder.ToTable("T_Articles");builder.Property(a => a.Title).HasMaxLength(100).IsUnicode().IsRequired();builder.Property(a => a.Message).IsUnicode().IsRequired();builder.HasQueryFilter(a => a.IsDeleted == false); // 增加筛选}}
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 使用全局过滤器
using System;
using System.Linq;
using System.Threading.Tasks;namespace OneToMany
{class Program{static async Task Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item in ctx.Articles) // 查询所有,但是已经屏蔽了Id为1的数据项{Console.WriteLine(item.Id + item.Price);}}Console.WriteLine();}}
}控制台输出:
587
588
589
590
591// SQL语句
SELECT [t].[Id], [t].[IsDeleted], [t].[Message], [t].[Price], [t].[Title]FROM [T_Articles] AS [t]WHERE [t].[IsDeleted] <> CAST(1 AS bit)说明:自动增加了IsDeleted过滤。
3.5 软删除的用法示例三
// 忽略全局过滤器
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;namespace OneToMany
{class Program{static async Task Main(string[] args){using (MyDbContext ctx = new MyDbContext()){foreach (var item in ctx.Articles.IgnoreQueryFilters()) // 忽略全局过滤器{Console.WriteLine(item.Id + item.Price);}}Console.WriteLine();}}
}控制台输出:
31
587
588
589
590
591// 查看SQLSELECT [t].[Id], [t].[IsDeleted], [t].[Message], [t].[Price], [t].[Title]FROM [T_Articles] AS [t]说明:忽略了IsDeleted过滤。
3.6 全局筛选的性能问题
- 可以通过索引或者其它方面的优化,遇到了再来解决。
- 傻瓜化的功能代表开发的时候不用关心细节问题,但不代表开发人员不需要知道相关知识点。
- 开发人员可以尽情享受傻瓜化的功能,但是遇到问题你要知道所以然。
结尾
书籍:ASP.NET Core技术内幕与项目实战
视频:https://www.bilibili.com/video/BV1pK41137He
著:杨中科
ISBN:978-7-115-58657-5
版次:第1版
发行:人民邮电出版社
※敬请购买正版书籍,侵删请联系85863947@qq.com※
※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※