本系列专栏基于杨中科老师的《ASP.NET Core技术内幕与项目实战》,本人记录梳理的学习笔记,有部分的增补和省略。更全面系统的讲解,请看杨老师的视频课:【.NET教程,.Net Core视频教程,杨中科主讲】。
一、全局查询筛选器
1. 定义
EF Core 全局查询筛选器是自动生效的过滤条件,一旦配置,所有针对该实体的 LINQ 查询、延迟加载、Include 关联查询都会自动附加这个筛选条件,无需手动重复编写过滤代码。
应用场景:软删除(逻辑删除)、多租户数据隔离、数据权限过滤。
2. 底层原理
EF Core 在生成 SQL 语句时,会自动拼接配置好的过滤条件到查询语句中,对开发者无感知,简化代码且避免漏写条件导致的业务错误。
3. 使用步骤
(1)实体类添加软删除标记
public class Book
{
public long Id { get; set; }
public string Title { get; set; }
// 软删除标记:false=未删除,true=已删除
public bool IsDeleted { get; set; }
}
(2)在 OnModelCreating 中配置筛选器
// Fluent API 配置全局查询筛选器
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 所有查询 Book 实体的操作,自动过滤掉已删除数据
modelBuilder.Entity<Book>()
.HasQueryFilter(b => b.IsDeleted == false);
}
(3)正常查询
// 自动拼接 AND IsDeleted = false
var books = dbContext.Books.Where(b => b.Title.Contains("EF Core")).ToList();
4. 临时忽略全局筛选器
场景:管理员需要查看已删除 + 未删除的所有数据时,可手动关闭筛选器:
// IgnoreQueryFilters():忽略当前实体配置的所有全局查询筛选器
var allBooks = dbContext.Books
.IgnoreQueryFilters()
.Where(b => b.Title.Contains("测试"))
.ToList();
5. 特性与注意事项
- 支持多条件组合:可同时配置软删除 + 多租户 ID 过滤
- 继承生效:父实体配置筛选器,子类自动继承
- 不影响插入 / 更新 / 删除:仅对查询操作生效
- 避免循环依赖:筛选器中不要编写跨实体循环引用逻辑
二、并发控制
当多个用户同时修改同一条数据时,会引发数据覆盖问题,EF Core 提供两种并发控制方案:悲观并发控制(锁定资源)和 乐观并发控制(无锁校验)。
1. 悲观并发控制
(1)定义
采用排他锁(行锁 / 表锁),在查询数据时就锁定资源,锁定期间其他用户无法修改 / 删除,直到事务提交释放锁。
(2)场景
并发量低、数据一致性要求极高的业务(如金融扣款、库存扣减)。
(3)示例
EF Core + MySQL 实现(FOR UPDATE 行锁)
// 开启事务
using (var transaction = await dbContext.Database.BeginTransactionAsync())
{
try
{
// 使用 FromSqlInterpolated 执行 FOR UPDATE,锁定指定行数据
var house = await dbContext.Houses
.FromSqlInterpolated($"SELECT * FROM T_Houses WHERE Id=1 FOR UPDATE")
.SingleAsync();
// 执行业务修改
house.Owner = "新户主";
await dbContext.SaveChangesAsync();
// 提交事务,释放锁
await transaction.CommitAsync();
}
catch (Exception ex)
{
// 回滚事务
await transaction.RollbackAsync();
Console.WriteLine($"操作失败:{ex.Message}");
}
}
- 优点:数据强一致,不会出现并发冲突
- 缺点:独占锁会阻塞其他请求,高并发场景下性能极差;使用不当极易引发死锁
2. 乐观并发控制(推荐)
(1)定义
不加锁,仅在执行更新 / 删除时,校验数据是否被其他用户修改;若已修改,抛出并发异常,由开发者处理冲突。核心思想是假设冲突极少发生,发生后再处理,性能最优。
(2)通用实现
自定义并发令牌(支持 MySQL / PostgreSQL 等所有数据库)。更新时拼接校验条件,只有满足条件才执行修改:
-- 只有 Owner 未被修改时,才更新数据
UPDATE T_Houses
SET Owner = '新值'
WHERE Id = 1 AND Owner = '旧值';
(3)具体步骤
1.实体类添加并发字段
public class House
{
public long Id { get; set; }
public string Owner { get; set; }
// 标记为并发令牌
public string Token { get; set; }
}
2.Fluent API 配置
modelBuilder.Entity<House>()
.Property(h => h.Token)
.IsConcurrencyToken(); // 标记为并发令牌
3.业务代码 + 异常捕获
try
{
// 修改数据
var house = dbContext.Houses.FirstOrDefault(h => h.Id == 1);
house.Owner = "新户主";
house.Token = Guid.NewGuid().ToString(); // 更新令牌
// 保存时自动校验并发
await dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// 捕获并发冲突异常
Console.WriteLine("数据已被其他用户修改,并发冲突!");
// 可选:获取冲突数据,实现自动重试/合并
var exceptionEntry = ex.Entries.First();
var currentValues = exceptionEntry.CurrentValues; // 当前修改的值
var databaseValues = exceptionEntry.GetDatabaseValues(); // 数据库最新值
}
(4)SQL Server 专用实现:ROWVERSION 类型
SQL Server 提供 rowversion(timestamp)字段,数据修改时自动更新,无需手动维护,是最优方案。
实现步骤:
实体类添加行版本字段
public class House
{
public long Id { get; set; }
public string Owner { get; set; }
// 行版本字段(SQL Server自动维护)
public byte[] RowVersion { get; set; }
}
Fluent API 配置
modelBuilder.Entity<House>()
.Property(h => h.RowVersion)
.IsRowVersion(); // 标记为行版本
直接使用(无需手动修改 RowVersion)
try
{
var house = dbContext.Houses.FirstOrDefault(h => h.Id == 1);
house.Owner = "新户主";
await dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine("并发冲突,操作失败");
}
总结
-
全局查询筛选器:自动过滤查询数据,软删除、多租户首选方案;配置:
HasQueryFilter(),临时关闭:IgnoreQueryFilters()。 -
并发控制选型:低并发、强一致性 → 悲观并发控制(排他锁);高并发、性能优先 → 乐观并发控制(推荐);MySQL:自定义
IsConcurrencyToken;SQL Server:IsRowVersion(最优);统一捕获异常:DbUpdateConcurrencyException。
378



被折叠的 条评论
为什么被折叠?



