如何处理数据加载器/GraphQL 嵌套查询中的并发 DbContext 访问?
Posted
技术标签:
【中文标题】如何处理数据加载器/GraphQL 嵌套查询中的并发 DbContext 访问?【英文标题】:How to handle concurrent DbContext access in dataloaders / GraphQL nested queries? 【发布时间】:2020-03-11 09:12:58 【问题描述】:我正在使用几个使用注入查询服务的数据加载器(这些服务又依赖于 DbContext)。它看起来像这样:
Field<ListGraphType<UserType>>(
"Users",
resolve: context =>
var loader = accessor.Context.GetOrAddBatchLoader<Guid, IEnumerable<User>>(
"MyUserLoader",
userQueryService.MyUserFunc);
return loader.LoadAsync(context.Source.UserId);
);
Field<ListGraphType<GroupType>>(
"Groups",
resolve: context =>
var loader = accessor.Context.GetOrAddBatchLoader<Guid, IEnumerable<Group>>(
"MyGroupLoader",
groupQueryService.MyGroupFunc);
return loader.LoadAsync(context.Source.GroupId);
);
当我运行同时使用两个数据加载器的嵌套查询时,我得到一个异常 "A second operation started on this context before a previous asynchronous operation completed"
,因为两个数据加载器同时使用相同的 DbContext。
在查询中允许并发数据库访问而无需使用ServiceLifeTime.Transient
仔细管理 DbContexts 的最佳方法是什么?或者数据加载器可以公开一种方法来知道何时处理瞬态 DbContexts?
【问题讨论】:
【参考方案1】:从“Scoped”切换到“Transient”无法解决问题,因为 Gql.Net 字段解析器是并行执行的。
根据您的示例,我希望您的 DbContext
被构造函数注入到您的“数据库服务”类(userQueryService
和 groupQueryService
)中,并且这些构造函数被注入到您的示例 GraphType 类中。因此,您的每个数据库服务都具有完全相同的 DbContext
范围副本。
解决方案是延迟解析您的DbContext
。
快速而简单的方法是使用“服务定位器”模式。
您将更改您的 db-services 以注入 IServiceScopeFactory
。然后你在你的加载器方法(MyUserFunc
和MyGroupFunc
)中使用它来创建一个作用域,然后解析你的DbContext
。这种方法(“服务定位器”)的问题在于,对 DbContext
的依赖隐藏在类中。
更好的方法(类似,但不是“服务定位器”)...
Use this relatively simple bit of code here on CodeReview.StackExchange 改为使用IServiceScopeFactory<T>
。您无需执行“服务定位器”即可获得延迟解析;您的强类型依赖项在构造函数中声明。
示例
所以假设你的 userQueryService
变量的类是这样的:
MyDbContext _dbContext;
public UserQueryService(MyDbContext dbContext) => _dbContext = dbContext;
public async Task<IDictionary<Guid, IEnumerable<User>> MyUserFunc(IEnumerable<Guid> userIds)
// code that uses _dbContext and returns the data...
把它改成这个(同样,使用IServiceScopeFactory<T>
):
IServiceScopeFactory<MyDbContext> _dbFactory;
public UserQueryService(IServiceScopeFactory<MyDbContext> dbFactory) => _dbFactory = dbFactory;
public async Task<IDictionary<Guid, IEnumerable<User>> MyUserFunc(IEnumerable<Guid> userIds)
using var scope = _dbFactory.CreateScope();
var dbContext = scope.GetRequiredService();
// code that uses dbContext and returns the data...
现在,当 Gql.Net 的解析器(在本例中为数据加载器)最终执行此方法时,您的 DbContext
的每次使用都使用它们自己的范围,因此它们不会像现在这样出现执行问题.
【讨论】:
感谢您的详细回答。实际上,我也将“db 服务”类设置为瞬态,因此我没有您认为我可能遇到的范围问题。不过,这看起来是一个更好的选择,当我有机会确认时,我会报告!【参考方案2】:Graphql 是一个注册为单例的中间件。
因为 dbcontext 在 graphql 中使用,如果您使用“AddDbContext”(即“作用域”),它也不是真正的“作用域”,因为它位于“单例”主中间件中!
所以,上下文只会在第一次被实例化!
您可以通过两种方法解决:
1-将 graphql 注册为“作用域”。但这意味着为每个请求加载所有 graphq 类型-->它很慢!
2-使用工厂模式来使用上下文,并显式地实例化
这里是使用 graphql .net core 的“现实生活”示例
https://github.com/graphql-dotnet/graphql-dotnet/issues/576#issuecomment-626661695
编辑:这里是解析器级别的最新实现 https://github.com/fenomeno83/graphql-dotnet-globalization-demo
【讨论】:
以上是关于如何处理数据加载器/GraphQL 嵌套查询中的并发 DbContext 访问?的主要内容,如果未能解决你的问题,请参考以下文章
如何处理 Vue.JS 中的 Apollo Graphql 查询错误?