GraphQL中结合GroupDataLoader实现多过滤条件的最佳方案咨询
在GraphQL中结合GroupDataLoader与多条件过滤器的实现方案
问题背景
我想在自身业务场景中,实现GraphQL查询结合多条件过滤器的功能。已知GraphQL没有原生Filter概念,目前我使用GroupDataLoader,传入Location就能获取对应所有记录,满足了基础查询需求。但现在需要添加Type、编号、供应商等额外搜索条件,而LoadAsync似乎只能传一个参数,因此有两个疑问:
- 是否可以同时使用GroupDataLoader和多个搜索过滤器?
- 如果不可行,最佳实践是什么?是获取所有记录后让UI/消费者筛选吗?
场景需求
查询期望:
- 获取地点为ATL的记录 => 返回3条记录
- 获取地点为ATL且类型为Car的记录 => 返回2条记录
- 获取地点为ATL、类型为Car且供应商为Hertz的记录 => 返回1条记录
现有代码示例
query.cs
[Authorize(policy: "ProjectAccess")] public async Task<IEnumerable<Exception>> GetExceptionsAsync( string location, [Service(ServiceKind.Synchronized)] IExceptionService exceptionService, long stockNumber = 0 ) { return await exceptionService.GetExceptionsByStockNumberV2(location); }
Service.cs
namespace Services { public interface IExceptionService { Task<IEnumerable<Database.Entities.Sql.Exception>> GetExceptionsByStockNumberV2( string location, long stockNumber = 0); } public class ExceptionService: ServiceBase, IExceptionService { private readonly IProjectXSupplyChainDbManagement _projectXSupplyChainDbManagement; private readonly ExceptionGroupDataLoader _exceptionDataLoader; public ExceptionService(ILogger<ExceptionService> logger, IProjectXSupplyChainDbManagement projectXSupplyChainDbManagement, IMapper mapper, ExceptionGroupDataLoader exceptionDataLoader) { _projectXSupplyChainDbManagement = projectXSupplyChainDbManagement; _exceptionDataLoader = exceptionDataLoader; } #region DATA LOADER public async Task<IEnumerable<Database.Entities.Sql.Exception>> GetExceptionsByStockNumberV2( string location, long stockNumber = 0 // 不确定如何将这类过滤器传递给dataLoader ) { var result = await _exceptionDataLoader.LoadAsync(location); return result; } #endregion DATA LOADER } }
GroupDataLoader.cs
namespace DataLoader; public class ExceptionGroupDataLoader : GroupedDataLoader<string, Exception> { private readonly IProjectXSupplyChainDbManagement _projectXSupplyChainDbManagement; public ExceptionGroupDataLoader( IBatchScheduler batchScheduler, IProjectXSupplyChainDbManagement projectXSupplyChainDbManagement, DataLoaderOptions options = null) : base(batchScheduler, options) { _projectXSupplyChainDbManagement = projectXSupplyChainDbManagement; } protected override async Task<ILookup<string, Exception>> LoadGroupedBatchAsync(IReadOnlyList<string> keys, CancellationToken cancellationToken) { var result = await _projectXSupplyChainDbManagement .ExceptionRepository.GetExceptionByStockNumbers(keys, cancellationToken) .ToLookupAsync(x => x.Location, cancellationToken); return result; } }
Repo.cs
namespace Repositories { public interface IExceptionRepository { public IAsyncEnumerable<Entities.Sql.Exception> GetExceptionByStockNumbers( IReadOnlyList<string> locations, CancellationToken cancellationToken); } public class ExceptionRepository: IExceptionRepository { private readonly ProjectXSqlDbContext _projectXSqlDbContext; public ExceptionRepository(ProjectXSqlDbContext projectXSqlDbContext) { _projectXSqlDbContext = projectXSqlDbContext; } public IAsyncEnumerable<Entities.Sql.Exception> GetExceptionByStockNumbers( IReadOnlyList<string> locations, CancellationToken cancellationToken) { var exceptions = _projectXSqlDbContext.Exceptions .Where(_ => locations.Contains(_.Location)) .AsAsyncEnumerable() // .ToDictionaryAsync(_ => _.Location, cancellationToken) ; return exceptions; } } }
解决方案
1. 可以同时使用GroupDataLoader和多搜索过滤器
核心思路是创建复合键,把所有需要过滤的字段组合成一个不可变的结构体或记录类型,作为GroupedDataLoader的键类型。这样LoadAsync就能接收包含所有过滤条件的复合键,DataLoader也能基于这些条件批量加载数据。
具体实现步骤:
第一步:定义过滤条件的复合键
使用C#记录类型(自动实现值相等性)定义包含所有过滤字段的键:
// 记录类型自动实现值相等,适合作为DataLoader的键 public record ExceptionFilterKey(string Location, long StockNumber, string? Type, string? Vendor);
第二步:修改GroupDataLoader的键类型
将ExceptionGroupDataLoader改为基于复合键的GroupedDataLoader:
namespace DataLoader; public class ExceptionGroupDataLoader : GroupedDataLoader<ExceptionFilterKey, Exception> { private readonly IProjectXSupplyChainDbManagement _projectXSupplyChainDbManagement; public ExceptionGroupDataLoader( IBatchScheduler batchScheduler, IProjectXSupplyChainDbManagement projectXSupplyChainDbManagement, DataLoaderOptions options = null) : base(batchScheduler, options) { _projectXSupplyChainDbManagement = projectXSupplyChainDbManagement; } protected override async Task<ILookup<ExceptionFilterKey, Exception>> LoadGroupedBatchAsync(IReadOnlyList<ExceptionFilterKey> keys, CancellationToken cancellationToken) { var exceptions = await _projectXSupplyChainDbManagement .ExceptionRepository.GetExceptionsByFilters(keys, cancellationToken) .ToListAsync(cancellationToken); // 将查询结果按复合键分组 return exceptions.ToLookup(e => new ExceptionFilterKey(e.Location, e.StockNumber, e.Type, e.Vendor), cancellationToken); } }
第三步:修改Repository的查询方法
更新IExceptionRepository和实现类,支持基于复合键集合的过滤:
namespace Repositories { public interface IExceptionRepository { public IAsyncEnumerable<Entities.Sql.Exception> GetExceptionsByFilters( IReadOnlyList<ExceptionFilterKey> filterKeys, CancellationToken cancellationToken); } public class ExceptionRepository: IExceptionRepository { private readonly ProjectXSqlDbContext _projectXSqlDbContext; public ExceptionRepository(ProjectXSqlDbContext projectXSqlDbContext) { _projectXSqlDbContext = projectXSqlDbContext; } public async IAsyncEnumerable<Entities.Sql.Exception> GetExceptionsByFilters( IReadOnlyList<ExceptionFilterKey> filterKeys, [EnumeratorCancellation] CancellationToken cancellationToken) { var query = _projectXSqlDbContext.Exceptions.AsQueryable(); // 构建动态查询:匹配所有过滤条件组合 query = query.Where(e => filterKeys.Any(k => k.Location == e.Location && (k.StockNumber == 0 || k.StockNumber == e.StockNumber) && (k.Type == null || k.Type == e.Type) && (k.Vendor == null || k.Vendor == e.Vendor))); await foreach (var exception in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) { yield return exception; } } } }
第四步:更新Service和GraphQL Query
修改ExceptionService的方法,传入所有过滤条件并创建复合键:
public async Task<IEnumerable<Database.Entities.Sql.Exception>> GetExceptionsByStockNumberV2( string location, long stockNumber = 0, string? type = null, string? vendor = null) { var filterKey = new ExceptionFilterKey(location, stockNumber, type, vendor); var result = await _exceptionDataLoader.LoadAsync(filterKey); return result; }
更新GraphQL Query方法,接收所有过滤参数:
[Authorize(policy: "ProjectAccess")] public async Task<IEnumerable<Exception>> GetExceptionsAsync( string location, [Service(ServiceKind.Synchronized)] IExceptionService exceptionService, long stockNumber = 0, string? type = null, string? vendor = null) { return await exceptionService.GetExceptionsByStockNumberV2(location, stockNumber, type, vendor); }
2. 不推荐客户端筛选的方案
如果暂时不想修改DataLoader实现,获取所有记录后让客户端筛选是可行的,但不推荐:
- 性能问题:会加载远超需求的数据,增加数据库和网络开销
- 资源浪费:占用不必要的内存和带宽
- 数据安全风险:可能返回客户端无权访问的数据
仅在数据量极小、过滤逻辑极其简单的临时场景下可以考虑,长期来看还是应该在服务端实现过滤。
内容的提问来源于stack exchange,提问作者GThree
相关产品推荐
相关产品推荐

