【问题标题】:graphql-spqr with Spring Boot and Transactional Boundary带有 Spring Boot 和事务边界的 graphql-spqr
【发布时间】:2020-07-07 10:40:42
【问题描述】:

我们正在将 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 并且我知道为什么:

主队由 ID 引用并由teamRepository 加载。返回的团队只是一个休眠代理。这对于保存新匹配很好,不需要更多。但是为了发回结果,GraphQL 需要访问代理并调用 match.team.getName()。但这显然发生在标有@Transactional 的事务之外。

我可以用 团队 homeTeam teamRepository.findById(matchForm.getHomeId()).orElse(null); match.setHomeTeam(homeTeam);

Hibernate 不再加载代理,而是加载真实对象。但是由于我不知道 GraphQL 查询到底要求什么,所以如果以后不需要,急切地加载所有数据是没有意义的。如果 GraphQL 能在 @Transactional 内执行就好了,这样我就可以为每个查询和突变定义事务边界。

对此有什么建议吗?

PS:我剥离了代码并进行了一些清理以使其更简洁。所以代码可能无法运行,但确实说明了问题。

【问题讨论】:

    标签: java hibernate graphql graphql-spqr graphql-spqr-spring-boot-starter


    【解决方案1】:

    @kaqqao 的回答非常好,但我想对此发表评论并展示第三种解决方案:

    1. 我不喜欢这种解决方案,因为我必须在我真的不关心它的情况下做很多工作。这不是 GraphQL 的意义所在。如果需要,应该进行解析。展望每一个 GraphQL 查询和路径的每一个方向对我来说听起来很奇怪。

    2. 我设法通过一个服务类来实现这一点。但是你会遇到异常问题,因为异常被包装并且没有被正确处理。

      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();
       }
      

      }

    1. 还有一种可能性,见https://spectrum.chat/graphql/general/transactional-queries-with-spring~47749680-3bb7-4508-8935-1d20d04d0c6a

    2. 目前我最喜欢的是拥有另一个手动解析器

       @GraphQLQuery
       @Transactional(readOnly = true)
       public Team getHomeTeam(@GraphQLContext Match match) {
           return matchRepository.getOne(match.getId()).getHomeTeam();
       }
      
    3. 当然你也可以设置spring.jpa.open-in-view=false(反模式)

    4. 或者您可以急切地获取。

    如果您可以使用 GraphQL 定义事务边界,那就太好了。

    【讨论】:

      【解决方案2】:

      我可以提供一些您可能需要考虑的事情。

      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 打到控制器方法上,就可以开始了。

      【讨论】:

      • 谢谢,很好的答案(和很棒的软件顺便说一句)如何交换控制器以及如何为查询设置只读标志并为突变设置读写?我不知道从哪里开始,但我会弄清楚的。
      猜你喜欢
      • 2017-05-22
      • 2020-08-19
      • 2021-08-31
      • 1970-01-01
      • 2021-07-25
      • 2016-06-05
      • 1970-01-01
      • 2020-10-01
      • 2020-12-25
      相关资源
      最近更新 更多