告别重复查询条件:EF Core全局查询过滤器让数据访问更智能

告别重复查询条件:EF Core全局查询过滤器让数据访问更智能

【免费下载链接】efcore efcore: 是 .NET 平台上一个开源的对象关系映射(ORM)框架,用于操作关系型数据库。适合开发者使用 .NET 进行数据库操作,简化数据访问和持久化过程。 【免费下载链接】efcore 项目地址: https://gitcode.com/GitHub_Trending/ef/efcore

你是否还在每个查询中重复编写"Where(isActive == true)"这样的条件?是否担心忘记过滤软删除数据导致业务异常?EF Core全局查询过滤器(Global Query Filters)提供了一种一劳永逸的解决方案,让查询条件自动应用于所有数据访问操作,彻底消除重复代码和潜在风险。读完本文,你将掌握如何在现有项目中快速实现查询过滤器、处理多租户场景、解决导航属性过滤问题,以及在特殊场景下临时禁用过滤器的技巧。

什么是全局查询过滤器

全局查询过滤器是EF Core提供的一种模型级别的查询条件配置机制,允许开发者为实体类型定义自动应用于所有查询的过滤条件。这些过滤器会在查询翻译阶段自动注入到生成的SQL语句中,确保无论通过何种方式访问数据(包括LINQ查询、延迟加载、显式加载等)都能应用统一的过滤逻辑。

官方实现位于src/EFCore/Metadata/Builders/EntityTypeBuilder.cs文件的HasQueryFilter方法,通过模型构建器配置后,EF Core会在查询处理过程中自动应用这些过滤条件。

基础实现:三步启用全局过滤

1. 准备实体类

首先确保实体类包含需要过滤的属性,如软删除标记:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; } // 软删除标记
    public bool IsActive { get; set; }  // 启用状态标记
}

2. 在模型构建时配置过滤器

在DbContext的OnModelCreating方法中,使用HasQueryFilter配置全局过滤条件:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 配置Product实体的全局查询过滤器
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted && p.IsActive);
        
    // 其他实体配置...
}

这段代码会为所有Product查询自动添加WHERE NOT IsDeleted AND IsActive = 1条件。实现逻辑可参考[src/EFCore/Metadata/Builders/EntityTypeBuilder1.cs](https://link.gitcode.com/i/a676d010689ef43ef12622b1688feeda)中的泛型版本HasQueryFilter`方法。

3. 验证过滤效果

配置完成后,任何针对Product的查询都会自动应用过滤条件:

// 以下查询会自动附加过滤条件
var activeProducts = dbContext.Products.ToList();
// 生成的SQL类似: SELECT * FROM Products WHERE IsDeleted = 0 AND IsActive = 1

即使使用Include加载关联数据,过滤器也会自动生效:

var ordersWithProducts = dbContext.Orders
    .Include(o => o.OrderItems)
    .ThenInclude(oi => oi.Product)
    .ToList();
// Product数据会自动过滤掉已删除和非活动的记录

高级应用:多租户数据隔离

在SaaS应用中,全局查询过滤器是实现多租户数据隔离的理想方案。通过结合依赖注入,可实现租户上下文的自动切换:

1. 注入租户服务

public class AppDbContext : DbContext
{
    private readonly ITenantService _tenantService;
    
    public AppDbContext(DbContextOptions<AppDbContext> options, ITenantService tenantService)
        : base(options)
    {
        _tenantService = tenantService;
    }
    
    // DbSet定义...
}

2. 配置租户过滤

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 获取当前租户ID
    var tenantId = _tenantService.GetCurrentTenantId();
    
    // 为所有租户实体应用过滤器
    modelBuilder.Entity<Customer>()
        .HasQueryFilter(c => c.TenantId == tenantId);
        
    modelBuilder.Entity<Order>()
        .HasQueryFilter(o => o.TenantId == tenantId);
        
    // 其他实体配置...
}

这种方式确保每个租户只能访问自己的数据,实现逻辑可参考测试用例test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs中的多过滤器组合示例。

处理关联数据:导航属性过滤

全局查询过滤器不仅作用于直接查询,还会自动应用于导航属性。例如,假设有以下实体关系:

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; }
    
    public ICollection<Product> Products { get; set; }
}

当同时为Category和Product配置过滤器:

modelBuilder.Entity<Category>()
    .HasQueryFilter(c => !c.IsDeleted);
    
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted && p.IsActive);

查询Category时会同时过滤自身和关联的Product:

var categories = dbContext.Categories
    .Include(c => c.Products)
    .ToList();
// 结果将只包含:
// 1. 未删除的Category
// 2. 每个Category下未删除且激活的Product

临时禁用过滤器

在某些特殊场景(如管理员数据管理界面),可能需要临时禁用全局查询过滤器。EF Core提供了IgnoreQueryFilters()方法实现这一需求:

// 管理员查看所有产品,包括已删除的
var allProducts = dbContext.Products
    .IgnoreQueryFilters()
    .ToList();
    
// 仅禁用特定查询的过滤器,其他实体过滤器仍生效
var categoriesWithAllProducts = dbContext.Categories
    .Include(c => c.Products.IgnoreQueryFilters())
    .ToList();

注意IgnoreQueryFilters()会禁用指定查询路径上的所有过滤器,请谨慎使用,确保有适当的权限控制。

多过滤器组合与优先级

EF Core支持为单个实体配置多个命名过滤器,并可通过键名进行覆盖或删除。如测试用例test/EFCore.Specification.Tests/Query/AdHocQueryFiltersQueryTestBase.cs所示:

modelBuilder.Entity<MyEntity>()
    .HasQueryFilter("NameFilter", x => x.Name.StartsWith("Name"))
    .HasQueryFilter("ActiveFilter", x => !x.IsDeleted)
    .HasQueryFilter("PublishedFilter", x => !x.IsDraft);

多个过滤器之间是AND关系,会组合生成WHERE Name LIKE 'Name%' AND IsDeleted = 0 AND IsDraft = 0的条件。若使用相同键名添加过滤器,则会覆盖原有过滤器。

性能考量

全局查询过滤器在查询翻译阶段应用,不会影响查询性能。EF Core会将过滤器条件与用户查询条件合并,生成优化的SQL语句。例如:

// 用户查询
var cheapActiveProducts = dbContext.Products
    .Where(p => p.Price < 100)
    .ToList();

生成的SQL会合并过滤器和用户条件:

SELECT * FROM Products 
WHERE IsDeleted = 0 AND IsActive = 1 AND Price < 100

这种合并不会导致额外的性能开销,与手动编写完整条件的性能相同。

常见陷阱与解决方案

1. 导航属性导致的意外过滤

当父实体和子实体都配置了过滤器时,可能会导致看似"数据丢失"的情况。例如:

// 父实体过滤器
modelBuilder.Entity<Order>()
    .HasQueryFilter(o => o.IsActive);
    
// 子实体过滤器
modelBuilder.Entity<OrderItem>()
    .HasQueryFilter(oi => oi.IsActive);

查询订单时,只会加载活跃订单中的活跃订单项。若需要查看所有订单项(包括非活跃的),应显式禁用子实体过滤器:

var orders = dbContext.Orders
    .Include(o => o.Items.IgnoreQueryFilters())
    .ToList();

2. 复杂表达式导致的客户端计算

避免在过滤器中使用EF Core无法翻译为SQL的复杂表达式,这会导致查询在客户端执行,降低性能:

// 不推荐 - 可能导致客户端评估
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => p.CreatedDate > DateTime.Now.AddDays(-30));
    
// 推荐 - 使用可翻译的表达式
var thirtyDaysAgo = DateTime.Now.AddDays(-30);
modelBuilder.Entity<Product>()
    .HasQueryFilter(p => p.CreatedDate > thirtyDaysAgo);

总结与最佳实践

全局查询过滤器是EF Core提供的强大功能,特别适合:

  • 软删除实现(IsDeleted = false
  • 多租户数据隔离(TenantId = @currentTenantId
  • 状态过滤(IsActive = true
  • 数据权限控制

最佳实践:

  1. 为每个过滤器添加明确注释,说明过滤目的
  2. 复杂过滤逻辑提取为私有方法,提高可读性
  3. 多租户场景使用依赖注入获取租户上下文
  4. 管理员功能中使用IgnoreQueryFilters()时,确保有严格的权限检查
  5. 利用命名过滤器(HasQueryFilter(string key, ...))实现动态开关

通过合理使用全局查询过滤器,可以显著减少重复代码,提高数据访问层的可维护性和安全性。这一功能的实现细节可在src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs中查看,核心是通过HasQueryFilter方法配置的查询条件会在查询翻译过程中自动注入。

掌握全局查询过滤器,让你的数据访问代码更简洁、更安全、更易于维护!

点赞+收藏+关注,获取更多EF Core实用技巧!下期预告:《EF Core性能优化实战:从索引到查询分析》

【免费下载链接】efcore efcore: 是 .NET 平台上一个开源的对象关系映射(ORM)框架,用于操作关系型数据库。适合开发者使用 .NET 进行数据库操作,简化数据访问和持久化过程。 【免费下载链接】efcore 项目地址: https://gitcode.com/GitHub_Trending/ef/efcore

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值