【问题标题】:LazyInitializationException with graphql-spring带有graphql-spring的LazyInitializationException
【发布时间】:2018-06-10 18:42:53
【问题描述】:

我目前正在将我的 REST-Server 迁移到 GraphQL(至少部分如此)。大部分工作已经完成,但我偶然发现了这个我似乎无法解决的问题:Graphql 查询中的 OneToMany 关系,使用 FetchType.LAZY。

我正在使用: https://github.com/graphql-java/graphql-spring-boothttps://github.com/graphql-java/graphql-java-tools 用于集成。

这是一个例子:

实体:

@Entity
class Show {
   private Long id;
   private String name;

   @OneToMany(mappedBy = "show")
   private List<Competition> competition;
}

@Entity
class Competition {
   private Long id;
   private String name;

   @ManyToOne(fetch = FetchType.LAZY)
   private Show show;
}

架构:

type Show {
    id: ID!
    name: String!
    competitions: [Competition]
}

type Competition {
    id: ID!
    name: String
}

extend type Query {
    shows : [Show]
}

解析器:

@Component
public class ShowResolver implements GraphQLQueryResolver {
    @Autowired    
    private ShowRepository showRepository;

    public List<Show> getShows() {
        return ((List<Show>)showRepository.findAll());
    }
}

如果我现在使用这个(速记)查询来查询端点:

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}

我明白了:

org.hibernate.LazyInitializationException: 延迟初始化失败 角色集合:Show.competitions,无法初始化代理 - 没有会话

现在我知道为什么会发生这个错误以及它意味着什么,但我真的不知道要为此应用修复程序。我不想让我的实体急切地获取所有关系,因为这会否定 GraphQL 的一些优势。我可能需要寻找解决方案的任何想法? 谢谢!

【问题讨论】:

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


    【解决方案1】:

    我解决了它,我想应该更仔细地阅读 graphql-java-tools 库的文档。 除了解决基本查询的GraphQLQueryResolver 之外,我还需要一个GraphQLResolver&lt;T&gt; 用于我的Showclass,它看起来像这样:

    @Component
    public class ShowResolver implements GraphQLResolver<Show> {
        @Autowired
        private CompetitionRepository competitionRepository;
    
        public List<Competition> competitions(Show show) {
            return ((List<Competition>)competitionRepository.findByShowId(show.getId()));
        }
    }
    

    这告诉库如何解析我的Showclass 中的复杂对象,并且仅在最初的查询请求包含Competitionobjects 时使用。新年快乐!

    EDIT 31.07.2019:我从此放弃了以下解决方案。长时间运行的事务很少是一个好主意,在这种情况下,一旦您扩展应用程序,它可能会导致问题。我们开始实现 DataLoaders 以异步方式批量查询。长时间运行的事务与 DataLoader 的异步特性相结合可能会导致死锁:https://github.com/graphql-java-kickstart/graphql-java-tools/issues/58#issuecomment-398761715(更多信息请参见上方和下方)。我不会删除下面的解决方案,因为对于较小的应用程序和/或不需要任何批处理查询的应用程序来说,它可能仍然是一个很好的起点,但在这样做时请牢记此注释。

    编辑:这里要求的是另一种使用自定义执行策略的解决方案。我正在使用graphql-spring-boot-startergraphql-java-tools

    创建一个 ExecutionStrategy 类型的 Bean 来处理事务,如下所示:

    @Service(GraphQLWebAutoConfiguration.QUERY_EXECUTION_STRATEGY)
    public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {
    
        @Override
        @Transactional
        public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
            return super.execute(executionContext, parameters);
        }
    }
    

    这会将查询的整个执行放在同一个事务中。我不知道这是否是最优化的解决方案,而且它在错误处理方面也已经存在一些缺点,但是您不需要那样定义类型解析器。

    请注意,如果这是唯一的 ExecutionStrategy Bean 存在,这也将用于突变,这与 Bean 名称可能暗示的相反。请参阅https://github.com/graphql-java-kickstart/graphql-spring-boot/blob/v11.1.0/graphql-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/web/boot/GraphQLWebAutoConfiguration.java#L161-L166 以供参考。为避免这种情况,请定义另一个 ExecutionStrategy 用于突变:

    @Bean(GraphQLWebAutoConfiguration.MUTATION_EXECUTION_STRATEGY)
    public ExecutionStrategy queryExecutionStrategy() {
        return new AsyncSerialExecutionStrategy();
    }
    

    【讨论】:

    • 看起来您正在使用双向一对多关联,因此您可以致电competitionRepository.findByShowId(show.getId())。这是您无需预先加载即可从节目实体访问比赛集合的唯一方法吗?
    • 我不确定您在问什么,但是如果没有比赛中的节目,就无法知道哪些比赛属于哪个节目。我认为虽然有一个开放的会话show.getCompetitions() 只是返回代理(惰性),然后如果需要一个完整的对象,也会像我所做的那样访问数据库。
    • @puelo 你AsyncTransactionalExecutionStrategy 已经是@Service 所以Spring 会为它创建一个bean。因此无需在executionStrategies() 方法中创建new AsyncTransactionalExecutionStrategy()。你应该简单地在那里注入AsyncTransactionalExecutionStrategy bean。
    • @puelo 你遇到过什么样的缺点?
    • 对特定QUERY执行策略的另一个改进是使用@Transactional(readOnly = true)
    【解决方案2】:

    对我来说,使用 AsyncTransactionalExecutionStrategy 会出现异常。例如。惰性初始化或应用程序级异常触发事务到仅回滚状态。然后 Spring 事务机制在策略 execute 的边界处抛出仅回滚事务,导致 HttpRequestHandlerImpl 返回 400 空响应。有关详细信息,请参阅 https://github.com/graphql-java-kickstart/graphql-java-servlet/issues/250https://github.com/graphql-java/graphql-java/issues/1652

    对我有用的是使用Instrumentation 将整个操作包装在一个事务中:https://spectrum.chat/graphql/general/transactional-queries-with-spring~47749680-3bb7-4508-8935-1d20d04d0c6a

    【讨论】:

      【解决方案3】:

      我首选的解决方案是在 Servlet 发送响应之前打开事务。通过这个小代码更改,您的 LazyLoad 将正常工作:

      import javax.servlet.Filter;
      import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;
      
      @SpringBootApplication
      public class Application {
      
        public static void main(String[] args) {
          SpringApplication.run(Application.class, args);
        }
      
        /**
         * Register the {@link OpenEntityManagerInViewFilter} so that the
         * GraphQL-Servlet can handle lazy loads during execution.
         *
         * @return
         */
        @Bean
        public Filter OpenFilter() {
          return new OpenEntityManagerInViewFilter();
        }
      
      }
      

      【讨论】:

        【解决方案4】:

        对于任何对接受的答案感到困惑的人,您需要更改 java 实体以包含双向关系,并确保使用辅助方法添加 Competition,否则很容易忘记正确设置关系。

        @Entity
        class Show {
           private Long id;
           private String name;
        
           @OneToMany(cascade = CascadeType.ALL, mappedBy = "show")
           private List<Competition> competition;
        
           public void addCompetition(Competition c) {
              c.setShow(this);
              competition.add(c);
           }
        }
        
        @Entity
        class Competition {
           private Long id;
           private String name;
        
           @ManyToOne(fetch = FetchType.LAZY)
           private Show show;
        }
        

        公认答案背后的一般直觉是:

        graphql 解析器ShowResolver 将打开一个事务以获取节目列表,但一旦完成,它将关闭事务。

        然后,competitions 的嵌套 graphql 查询将尝试在从上一个查询中检索到的每个 Show 实例上调用 getCompetition(),这将引发 LazyInitializationException,因为事务已关闭。

        {
          shows {
            id
            name
            competitions {
              id
            }
          }
        }
        

        接受的答案基本上是 绕过通过OneToMany 关系检索比赛列表,而是在新事务中创建新查询,从而消除了问题。

        不确定这是否是黑客攻击,但解析器上的 @Transactional 对我不起作用,尽管这样做的逻辑确实有些道理,但我显然不了解根本原因。

        【讨论】:

        • ShowResolver 上的 @Transactional 不起作用,因为当 GraphQL 尝试解决竞争时,事务已经关闭。我目前正在使用另一种解决方案(我也不确定它是否是最佳的):我定义了一个自定义ExecutionStrategy(基本上与AsyncExecutionStrategy相同)其中execute方法用@Transactional注释.如果需要,我可以更新我的答案。
        • @puelo,请这样做,因为我不知道如何在 graphql java 中做任何事情。似乎根本没有记录。
        • 我将它添加到我的原始答案中。
        【解决方案5】:

        您只需要使用@Transactional 注释您的解析器类。然后,从存储库返回的实体将能够延迟获取数据。

        【讨论】:

        • 你确定吗?一旦解析器方法完成执行,会话是否会关闭,因此当 GraphQL DataFetcher 在 @OneToMany 关系实体上执行任何 getter/type-resolver 时仍然会失败?至少这是我的经验。
        • 好的,我没有针对这种情况进行测试,但至少它允许您在解析器方法中使用延迟加载的集合(否则会产生同样的异常)。
        【解决方案6】:

        我假设每当您获取 Show 的对象时,您都想要 Show 对象的所有关联 Competition

        默认情况下,实体中所有集合类型的获取类型为 LAZY。您可以指定 EAGER 类型以确保 hibernate 获取集合。

        在您的 Show 类中,您可以将 fetchType 更改为 EAGER

        @OneToMany(cascade=CascadeType.ALL,fetch=FetchType.EAGER)
        private List<Competition> competition;
        

        【讨论】:

        • 不,那是我不想要的,因为我还希望能够在没有比赛的情况下查询所有节目
        • OP 在他的问题中说他不想让所有的收藏都急切
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-03-26
        • 2014-08-08
        • 2012-01-18
        • 2011-05-21
        • 2014-02-25
        • 2011-02-13
        • 2019-05-19
        相关资源
        最近更新 更多