在HotChocolate解析器中获取当前Poll实例ID并实现userHasAnswered字段
问题解答
1. 获取当前Poll的ID
在Hot Chocolate的字段解析器中,你可以通过context.Parent<Poll>()获取当前正在处理的Poll实体实例,直接从中读取Id属性,修正后的代码如下:
public class PollType : ObjectType<Poll> { protected override void Configure(IObjectTypeDescriptor<Poll> descriptor) { descriptor .Field("userHasAnswered") .Resolve(context => { var currentPoll = context.Parent<Poll>(); // 获取当前Poll实例 var dataContext = context.Service<DataContext>(); var userId = context.Service<IHttpContextAccessor>().HttpContext.User.GetUserId(); return dataContext.Answers.Any(x => x.UserId == userId && x.Question.PollId == currentPoll.Id); }); } }
2. 当前实现的问题与优化方案
当前方案的不足
- N+1查询问题:如果一次查询返回多个
Poll,每个Poll都会触发单独的Answers.Any()查询,数据库请求次数会随返回的Poll数量线性增长,性能低下。 - 业务逻辑耦合:判断用户是否参与投票的逻辑直接写在解析器中,难以单独编写单元测试,不符合关注点分离的设计原则。
- 依赖Web环境:直接依赖
IHttpContextAccessor,导致代码无法脱离Web环境进行测试,灵活性不足。
更好的实现方案
方案一:使用DataLoader解决N+1问题
Hot Chocolate的DataLoader支持批量查询,可以将多个Poll的判断请求合并为一次数据库查询:
首先定义批量查询的DataLoader:
public class UserPollAnswerStatusDataLoader : BatchDataLoader<int, bool> { private readonly DataContext _dbContext; private readonly IHttpContextAccessor _httpContextAccessor; public UserPollAnswerStatusDataLoader( IBatchScheduler batchScheduler, DataContext dbContext, IHttpContextAccessor httpContextAccessor) : base(batchScheduler) { _dbContext = dbContext; _httpContextAccessor = httpContextAccessor; } protected override async Task<IReadOnlyDictionary<int, bool>> LoadBatchAsync( IReadOnlyList<int> pollIds, CancellationToken cancellationToken) { var userId = _httpContextAccessor.HttpContext.User.GetUserId(); if (userId == null) { return pollIds.ToDictionary(id => id, _ => false); } // 批量查询用户参与过的Poll ID var answeredPollIds = await _dbContext.Answers .Where(a => a.UserId == userId && pollIds.Contains(a.Question.PollId)) .Select(a => a.Question.PollId) .Distinct() .ToListAsync(cancellationToken); return pollIds.ToDictionary(id => id, answeredPollIds.Contains); } }
然后在解析器中使用该DataLoader:
public class PollType : ObjectType<Poll> { protected override void Configure(IObjectTypeDescriptor<Poll> descriptor) { descriptor .Field("userHasAnswered") .Resolve(async context => { var currentPoll = context.Parent<Poll>(); var dataLoader = context.DataLoader<UserPollAnswerStatusDataLoader>(); return await dataLoader.LoadAsync(currentPoll.Id); }); } }
方案二:将业务逻辑抽离到服务层
把判断用户是否参与投票的逻辑封装到独立的业务服务中,让解析器只负责调用服务,保持职责单一:
public interface IPollService { Task<bool> HasUserAnsweredPollAsync(int pollId, int userId); } public class PollService : IPollService { private readonly DataContext _dbContext; public PollService(DataContext dbContext) { _dbContext = dbContext; } public async Task<bool> HasUserAnsweredPollAsync(int pollId, int userId) { return await _dbContext.Answers .AnyAsync(a => a.UserId == userId && a.Question.PollId == pollId); } }
修改解析器调用服务:
public class PollType : ObjectType<Poll> { protected override void Configure(IObjectTypeDescriptor<Poll> descriptor) { descriptor .Field("userHasAnswered") .Resolve(async context => { var currentPoll = context.Parent<Poll>(); var pollService = context.Service<IPollService>(); var userId = context.Service<IHttpContextAccessor>().HttpContext.User.GetUserId(); return await pollService.HasUserAnsweredPollAsync(currentPoll.Id, userId); }); } }
方案三:移除对IHttpContextAccessor的直接依赖
Hot Chocolate提供了直接获取当前用户ClaimsPrincipal的方式,可以替代IHttpContextAccessor,让代码更易测试:
public class PollType : ObjectType<Poll> { protected override void Configure(IObjectTypeDescriptor<Poll> descriptor) { descriptor .Field("userHasAnswered") .Resolve(async context => { var currentPoll = context.Parent<Poll>(); var user = context.User; // 直接获取当前用户ClaimsPrincipal var userId = user.GetUserId(); var pollService = context.Service<IPollService>(); return await pollService.HasUserAnsweredPollAsync(currentPoll.Id, userId); }); } }
内容的提问来源于stack exchange,提问作者Cameron Cheung
相关产品推荐
相关产品推荐

