具有 Spring 引导和事务边界的 graphql-spqr
graphql-spqr with Spring Boot and Transactional Boundary
我们正在为新项目使用 graphql-spqr 和 graphql-spqr-spring-boot-starter(使用 Spring DataJPA、hibernate 等)。
我们有这样的突变:
@Transactional
@GraphQLMutation
public Match createMatch(@NotNull @Valid MatchForm matchForm) {
Match match = new Match(matchForm.getDate());
match.setHomeTeam(teamRepository.getOne(matchForm.getHomeId()));
match.setAwayTeam(teamRepository.getOne(matchForm.getAwayId()));
match.setResult(matchForm.getResult());
matchRepository.save(match);
return match;
}
这个突变工作正常:
mutation createMatch ($matchForm: MatchFormInput!){
match: createMatch(matchForm: $matchForm) {
id
}
variables: {...}
我省略了变量,因为它们不重要。如果我将其更改为:
它不起作用
mutation createMatch ($matchForm: MatchFormInput!){
match: createMatch(matchForm: $matchForm) {
id
homeTeam {
name
}
}
variables: {...}
我得到一个 LazyInitalizationException 并且我知道原因:
homeTeam 由 ID 引用并由 teamRepository
加载。返回的团队只是一个休眠代理。这对于保存新的比赛很好,不需要更多。但是为了发回结果,GraphQL 需要访问代理并调用 match.team.getName()。但这显然发生在标有 @Transactional
.
的交易之外
我可以用
团队主队 teamRepository.findById(matchForm.getHomeId()).orElse(null);
match.setHomeTeam(主队);
Hibernate 不再加载代理而是加载真实对象。但由于我不知道 GraphQL 查询到底要求什么,如果以后不需要,急切加载所有数据是没有意义的。如果 GraphQL 能在 @Transactional
内执行就好了,这样我就可以为每个查询和变更定义事务边界。
对此有什么建议吗?
PS:我剥离了代码并进行了一些清理以使其更简洁。所以代码可能无法运行,但确实说明了问题。
我可以提出一些您可能需要考虑的事项。
1) 根据需要预先加载
你总是可以抢先检查查询需要哪些字段并提前加载它们。详细信息在 graphql-java 博客的 Building efficient data fetchers by looking ahead 文章中有很好的解释。
简而言之,您可以拨打电话 DataFetchingEnvironment#getSelectionSet()
,它将为您提供 DataFetchingFieldSelectionSet
,其中包含优化加载所需的所有信息。
在 SPQR 中,您始终可以通过注入 ResolutionEnvironment
:
获得 DataFetchingEnvironment
(以及更多)
@GraphQLMutation
public Match createMatch(
@NotNull @Valid MatchForm matchForm,
@GraphQLEnvironment ResolutionEnvironment env) { ... }
如果只需要一级子字段的名称,可以注入
@GraphQLEnvironment Set<String> selection
相反。
为了可测试性,您始终可以连接自己的 ArgumentInjector
以精确挑选您需要注入的内容,因此在测试中更容易模拟。
2) 运行 事务中的整个 GraphQL 查询解析
除了在单个解析器上具有 @Transactional
之外,您还可以将默认控制器替换为在事务中运行整个事物的控制器。只需将 @Transactional
添加到控制器方法中,就可以开始了。
@kaqqao 回答很好,但我想评论这些并展示第三种解决方案:
我不喜欢这个解决方案,因为我必须在一个我真的不关心它的环境中做很多工作。这不是 GraphQL 的目的。如果需要,应该进行解决。在每个 GraphQL 查询和路径的每个方向上向前看对我来说听起来很奇怪。
我设法通过服务 class 实现了这一点。但是你会遇到异常问题,因为异常被包装并且没有被正确处理。
public class TransactionalGraphQLExecutor implements GraphQLServletExecutor {
private final ServletContextFactory contextFactory;
@Autowired(required = false)
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
private DataLoaderRegistryFactory dataLoaderRegistryFactory;
private final TxGraphQLExecutor txGraphQLExecutor;
public TransactionalGraphQLExecutor(ServletContextFactory contextFactory, TxGraphQLExecutor txGraphQLExecutor) {
this.contextFactory = contextFactory;
this.txGraphQLExecutor = txGraphQLExecutor;
}
@Override
public Map<String, Object> execute(GraphQL graphQL, GraphQLRequest graphQLRequest, NativeWebRequest nativeRequest) {
ExecutionInput executionInput = buildInput(graphQLRequest, nativeRequest, contextFactory, dataLoaderRegistryFactory);
if (graphQLRequest.getQuery().startsWith("mutation")) {
return txGraphQLExecutor.executeReadWrite(graphQL, executionInput);
} else {
return txGraphQLExecutor.executeReadOnly(graphQL, executionInput);
}
}
}
public class TxGraphQLExecutor {
@Transactional
public Map<String, Object> executeReadWrite(GraphQL graphQL, ExecutionInput executionInput) {
return graphQL.execute(executionInput).toSpecification();
}
@Transactional(readOnly = true)
public Map<String, Object> executeReadOnly(GraphQL graphQL, ExecutionInput executionInput) {
return graphQL.execute(executionInput).toSpecification();
}
}
-
目前我最喜欢的是多一个手动解析器
@GraphQLQuery
@Transactional(readOnly = true)
public Team getHomeTeam(@GraphQLContext Match match) {
return matchRepository.getOne(match.getId()).getHomeTeam();
}
当然你也可以设置spring.jpa.open-in-view=false
(反模式)
或者你可以急切获取
如果你能用 GraphQL 定义事务边界就好了。
我们正在为新项目使用 graphql-spqr 和 graphql-spqr-spring-boot-starter(使用 Spring DataJPA、hibernate 等)。
我们有这样的突变:
@Transactional
@GraphQLMutation
public Match createMatch(@NotNull @Valid MatchForm matchForm) {
Match match = new Match(matchForm.getDate());
match.setHomeTeam(teamRepository.getOne(matchForm.getHomeId()));
match.setAwayTeam(teamRepository.getOne(matchForm.getAwayId()));
match.setResult(matchForm.getResult());
matchRepository.save(match);
return match;
}
这个突变工作正常:
mutation createMatch ($matchForm: MatchFormInput!){
match: createMatch(matchForm: $matchForm) {
id
}
variables: {...}
我省略了变量,因为它们不重要。如果我将其更改为:
它不起作用mutation createMatch ($matchForm: MatchFormInput!){
match: createMatch(matchForm: $matchForm) {
id
homeTeam {
name
}
}
variables: {...}
我得到一个 LazyInitalizationException 并且我知道原因:
homeTeam 由 ID 引用并由 teamRepository
加载。返回的团队只是一个休眠代理。这对于保存新的比赛很好,不需要更多。但是为了发回结果,GraphQL 需要访问代理并调用 match.team.getName()。但这显然发生在标有 @Transactional
.
我可以用 团队主队 teamRepository.findById(matchForm.getHomeId()).orElse(null); match.setHomeTeam(主队);
Hibernate 不再加载代理而是加载真实对象。但由于我不知道 GraphQL 查询到底要求什么,如果以后不需要,急切加载所有数据是没有意义的。如果 GraphQL 能在 @Transactional
内执行就好了,这样我就可以为每个查询和变更定义事务边界。
对此有什么建议吗?
PS:我剥离了代码并进行了一些清理以使其更简洁。所以代码可能无法运行,但确实说明了问题。
我可以提出一些您可能需要考虑的事项。
1) 根据需要预先加载
你总是可以抢先检查查询需要哪些字段并提前加载它们。详细信息在 graphql-java 博客的 Building efficient data fetchers by looking ahead 文章中有很好的解释。
简而言之,您可以拨打电话 DataFetchingEnvironment#getSelectionSet()
,它将为您提供 DataFetchingFieldSelectionSet
,其中包含优化加载所需的所有信息。
在 SPQR 中,您始终可以通过注入 ResolutionEnvironment
:
DataFetchingEnvironment
(以及更多)
@GraphQLMutation
public Match createMatch(
@NotNull @Valid MatchForm matchForm,
@GraphQLEnvironment ResolutionEnvironment env) { ... }
如果只需要一级子字段的名称,可以注入
@GraphQLEnvironment Set<String> selection
相反。
为了可测试性,您始终可以连接自己的 ArgumentInjector
以精确挑选您需要注入的内容,因此在测试中更容易模拟。
2) 运行 事务中的整个 GraphQL 查询解析
除了在单个解析器上具有 @Transactional
之外,您还可以将默认控制器替换为在事务中运行整个事物的控制器。只需将 @Transactional
添加到控制器方法中,就可以开始了。
@kaqqao 回答很好,但我想评论这些并展示第三种解决方案:
我不喜欢这个解决方案,因为我必须在一个我真的不关心它的环境中做很多工作。这不是 GraphQL 的目的。如果需要,应该进行解决。在每个 GraphQL 查询和路径的每个方向上向前看对我来说听起来很奇怪。
我设法通过服务 class 实现了这一点。但是你会遇到异常问题,因为异常被包装并且没有被正确处理。
public class TransactionalGraphQLExecutor implements GraphQLServletExecutor { private final ServletContextFactory contextFactory; @Autowired(required = false) @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") private DataLoaderRegistryFactory dataLoaderRegistryFactory; private final TxGraphQLExecutor txGraphQLExecutor; public TransactionalGraphQLExecutor(ServletContextFactory contextFactory, TxGraphQLExecutor txGraphQLExecutor) { this.contextFactory = contextFactory; this.txGraphQLExecutor = txGraphQLExecutor; } @Override public Map<String, Object> execute(GraphQL graphQL, GraphQLRequest graphQLRequest, NativeWebRequest nativeRequest) { ExecutionInput executionInput = buildInput(graphQLRequest, nativeRequest, contextFactory, dataLoaderRegistryFactory); if (graphQLRequest.getQuery().startsWith("mutation")) { return txGraphQLExecutor.executeReadWrite(graphQL, executionInput); } else { return txGraphQLExecutor.executeReadOnly(graphQL, executionInput); } }
}
public class TxGraphQLExecutor { @Transactional public Map<String, Object> executeReadWrite(GraphQL graphQL, ExecutionInput executionInput) { return graphQL.execute(executionInput).toSpecification(); } @Transactional(readOnly = true) public Map<String, Object> executeReadOnly(GraphQL graphQL, ExecutionInput executionInput) { return graphQL.execute(executionInput).toSpecification(); }
}
目前我最喜欢的是多一个手动解析器
@GraphQLQuery @Transactional(readOnly = true) public Team getHomeTeam(@GraphQLContext Match match) { return matchRepository.getOne(match.getId()).getHomeTeam(); }
当然你也可以设置
spring.jpa.open-in-view=false
(反模式)或者你可以急切获取
如果你能用 GraphQL 定义事务边界就好了。