HotChocolate v13中[UseProjections]与DataLoaders协同失效问题求助
我使用以下GraphQL查询,希望仅从数据库提取Host对象的hostId、hostName和siteId三个字段:
query { listTenants { totalCount items { tenantId name sites { totalCount items { siteId cmxName cmxState hosts( order: { hostId: ASC } where: { hostName: { neq: "ans" } } skip: 4 take: 2 ) { totalCount items { hostId hostName siteId } } } } } } }
我通过扩展Site对象类型实现Hosts查询:
namespace dataGraphAPI.Types.Nodes { [ExtendObjectType<Site>] public static class SiteNode { [GraphQLName(SchemaConstants.Hosts)] [ListQueries] public static async Task<IEnumerable<Host>> GetHostsAsync( [Parent] Site site, IHostsBySiteIdDataLoader dataLoader, CancellationToken ct) => await dataLoader.LoadAsync(site.SiteId.ToString(), ct); } }
ListQueries属性配置了分页、投影、过滤和排序:
using HotChocolate.Types.Descriptors; using System.Reflection; namespace dataGraphAPI.Types { public sealed class ListQueriesAttribute : ObjectFieldDescriptorAttribute { protected override void OnConfigure(IDescriptorContext context, IObjectFieldDescriptor descriptor, MemberInfo member) { ApplyAttribute( context, descriptor, member, new UseOffsetPagingAttribute() { IncludeTotalCount = true, }); ApplyAttribute( context, descriptor, member, new UseProjectionAttribute()); ApplyAttribute( context, descriptor, member, new UseFilteringAttribute()); ApplyAttribute( context, descriptor, member, new UseSortingAttribute()); } } }
DataLoader实现如下:
[DataLoader] internal static async Task<ILookup<string, Host>> GetHostsBySiteIdAsync(IReadOnlyList<string> siteIds, CmxDbContext dbContext, CancellationToken ct) { var hosts = dbContext.Hosts .Where(x => siteIds.Contains(x.SiteId.ToString())); return hosts.ToLookup(x => x.SiteId.ToString()!); }
Program.cs的GraphQL配置:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<CmxDbContext>(options => options.UseNpgsql(builder.Configuration.GetConnectionString("CMXContext")) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) .LogTo(Console.WriteLine, LogLevel.Information)); builder.Services .AddGraphQLServer() .AddTypes() .AddType<AggregateResult>() .AddType<CountResult>() .AddType<DistinctResult>() .AddDirectiveType<AggregateDirectiveType>() .AddDirectiveType<CountDirectiveType>() .AddDirectiveType<DistinctDirectiveType>() .AddFiltering() .AddSorting() .AddProjections() .RegisterDbContext<CmxDbContext>(); var app = builder.Build(); app.MapGraphQL(); app.Run();
目前遇到的问题:UseProjection属性未生效,数据库返回了Host的所有字段而非指定的三个。我知道UseProjection对IQueryable有效,但DataLoader无法返回IQueryable,请问如何让投影功能与DataLoader协同工作?
核心思路是让HotChocolate把GraphQL投影逻辑传递到数据库查询层面,同时保留DataLoader的批量加载能力,推荐以下简洁实现方式:
推荐方案:使用UseDataLoader属性替代手动调用DataLoader
这种方式无需手动处理投影传递,HotChocolate会自动将投影、过滤、排序转换为EF Core查询语句,同时通过DataLoader批量加载数据:
1. 修改SiteNode的GetHosts方法
将方法返回类型改为IQueryable<Host>,并添加UseDataLoader属性:
namespace dataGraphAPI.Types.Nodes { [ExtendObjectType<Site>] public static class SiteNode { [GraphQLName(SchemaConstants.Hosts)] [ListQueries] // 保留原有分页、投影、过滤、排序配置 [UseDataLoader(typeof(HostsBySiteIdDataLoader))] // 启用批量加载 public static IQueryable<Host> GetHosts([Parent] Site site, CmxDbContext dbContext) { // 返回当前Site对应的Host查询IQueryable,后续由HotChocolate处理投影、过滤和批量加载 return dbContext.Hosts.Where(x => x.SiteId == site.SiteId); } } }
2. 优化DataLoader实现
使用SiteId的原始类型(比如Guid,避免字符串转换的性能损耗),批量查询时直接匹配原始值:
[DataLoader] internal static async Task<ILookup<Guid, Host>> GetHostsBySiteIdAsync(IReadOnlyList<Guid> siteIds, CmxDbContext dbContext, CancellationToken ct) { var hosts = await dbContext.Hosts .Where(x => siteIds.Contains(x.SiteId)) .ToListAsync(ct); return hosts.ToLookup(x => x.SiteId); }
3. 效果验证
执行GraphQL查询时,HotChocolate会自动完成以下操作:
- 解析查询中的投影字段,生成EF Core的
Select表达式,仅查询hostId、hostName、siteId - 收集所有请求的SiteId,通过DataLoader批量查询数据库
- 将批量查询结果分配给对应的Site请求,仅返回投影指定的字段
原方案失效原因
之前手动调用DataLoader时,UseProjection无法将投影逻辑传递到DataLoader内部的EF Core查询中,DataLoader返回的IEnumerable<Host>只能在内存中做投影,导致数据库加载了Host的所有字段。而使用UseDataLoader属性时,HotChocolate会先处理投影、过滤、排序逻辑,再执行批量查询,确保这些操作都在数据库层面完成。
内容的提问来源于stack exchange,提问作者Daniel Ninchev

