graphql-spring的LazyInitializationException

1bqhqjot  于 2023-08-02  发布在  Spring
关注(0)|答案(7)|浏览(92)

我目前正在将我的REST服务器迁移到GraphQL(至少部分)。大部分的工作已经完成,但我偶然发现了这个我似乎无法解决的问题:Graphql查询中的OneToMany关系,使用FetchType. LAZY。
我使用:https://github.com/graphql-java/graphql-spring-boothttps://github.com/graphql-java/graphql-java-tools进行集成。
下面是一个示例:

实体:

  1. @Entity
  2. class Show {
  3. private Long id;
  4. private String name;
  5. @OneToMany(mappedBy = "show")
  6. private List<Competition> competition;
  7. }
  8. @Entity
  9. class Competition {
  10. private Long id;
  11. private String name;
  12. @ManyToOne(fetch = FetchType.LAZY)
  13. private Show show;
  14. }

字符串

架构:

  1. type Show {
  2. id: ID!
  3. name: String!
  4. competitions: [Competition]
  5. }
  6. type Competition {
  7. id: ID!
  8. name: String
  9. }
  10. extend type Query {
  11. shows : [Show]
  12. }

解析器:

  1. @Component
  2. public class ShowResolver implements GraphQLQueryResolver {
  3. @Autowired
  4. private ShowRepository showRepository;
  5. public List<Show> getShows() {
  6. return ((List<Show>)showRepository.findAll());
  7. }
  8. }


如果我现在用这个(简写)查询端点:

  1. {
  2. shows {
  3. id
  4. name
  5. competitions {
  6. id
  7. }
  8. }
  9. }


我得到:
org.hibernate.LazyInitializationException:无法延迟初始化角色的集合:Show.competitions,cannot initialize proxy - no Session
现在我知道为什么会发生这个错误,这意味着什么,但我真的不知道我们要应用这个修复程序。我不想让我的实体急切地获取所有关系,因为这会否定GraphQL的一些优点。有什么想法我可能需要寻找一个解决方案?谢谢你,谢谢

ni65a41a

ni65a41a1#

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

  1. import javax.servlet.Filter;
  2. import org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter;
  3. @SpringBootApplication
  4. public class Application {
  5. public static void main(String[] args) {
  6. SpringApplication.run(Application.class, args);
  7. }
  8. /**
  9. * Register the {@link OpenEntityManagerInViewFilter} so that the
  10. * GraphQL-Servlet can handle lazy loads during execution.
  11. *
  12. * @return
  13. */
  14. @Bean
  15. public Filter OpenFilter() {
  16. return new OpenEntityManagerInViewFilter();
  17. }
  18. }

字符串

展开查看全部
wgx48brx

wgx48brx2#

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

  1. @Component
  2. public class ShowResolver implements GraphQLResolver<Show> {
  3. @Autowired
  4. private CompetitionRepository competitionRepository;
  5. public List<Competition> competitions(Show show) {
  6. return ((List<Competition>)competitionRepository.findByShowId(show.getId()));
  7. }
  8. }

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

编辑31.07.2019:从那以后,我就离开了下面的解决方案。长时间运行的事务很少是一个好主意,在这种情况下,一旦扩展应用程序,它可能会导致问题。我们开始实现DataLoaders,以异步方式批处理查询。长时间运行的事务与DataLoaders的异步特性相结合可能导致死锁:https://github.com/graphql-java-kickstart/graphql-java-tools/issues/58#issuecomment-398761715(以上和以下获取更多信息)。我不会删除下面的解决方案,因为它可能仍然是小型应用程序和/或不需要任何批量查询的应用程序的良好起点,但请在这样做时记住这一点。
**编辑:**此处请求的是使用自定义执行策略的另一种解决方案。我正在使用graphql-spring-boot-startergraphql-java-tools

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

  1. @Service(GraphQLWebAutoConfiguration.QUERY_EXECUTION_STRATEGY)
  2. public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {
  3. @Override
  4. @Transactional
  5. public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
  6. return super.execute(executionContext, parameters);
  7. }
  8. }


这将查询的整个执行放在同一个事务中。我不知道这是否是最佳的解决方案,而且它在错误处理方面也有一些缺点,但您不需要以这种方式定义类型解析器。
请注意,如果这是唯一存在的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用于突变:

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

展开查看全部
rnmwe5a2

rnmwe5a23#

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

  1. @Entity
  2. class Show {
  3. private Long id;
  4. private String name;
  5. @OneToMany(cascade = CascadeType.ALL, mappedBy = "show")
  6. private List<Competition> competition;
  7. public void addCompetition(Competition c) {
  8. c.setShow(this);
  9. competition.add(c);
  10. }
  11. }
  12. @Entity
  13. class Competition {
  14. private Long id;
  15. private String name;
  16. @ManyToOne(fetch = FetchType.LAZY)
  17. private Show show;
  18. }

字符串
公认答案背后的一般直觉是:
graphql解析器ShowResolver将打开一个事务以获取节目列表,但一旦完成,它将关闭该事务。
然后competitions的嵌套graphql查询将尝试调用getCompetition()上的每个Show示例,因为事务已经关闭,这将抛出LazyInitializationException

  1. {
  2. shows {
  3. id
  4. name
  5. competitions {
  6. id
  7. }
  8. }
  9. }


接受的答案基本上是绕过通过OneToMany关系检索竞争列表,而是在新事务中创建新查询,从而消除了问题。
不确定这是否是一个黑客,但@Transactional解析器不为我工作,虽然这样做的逻辑确实有一些意义,但我显然不明白根本原因。

展开查看全部
kx1ctssn

kx1ctssn4#

对我来说,使用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将整个操作 Package 在一个事务中:https://spectrum.chat/graphql/general/transactional-queries-with-spring~47749680-3bb7-4508-8935-1d20d04d0c6a

ruarlubt

ruarlubt5#

正如Oleg所指出的,使用AsyncTransactionalExecutionStrategy的错误处理在嵌套事务中被破坏了。
由于他的答案中的URL不再起作用,以下是我如何解决的。
首先,让我们来看看我想通过GraphQL响应正确处理的一些异常

  1. public class UserFriendlyException extends RuntimeException {
  2. public UserFriendlyException(String message) {
  3. super(message);
  4. }
  5. }

字符串
然后我定义了错误响应

  1. public class UserFriendlyGraphQLError implements GraphQLError {
  2. /** Message shown to user */
  3. private final String message;
  4. private final List<SourceLocation> locations;
  5. private final ExecutionPath path;
  6. public UserFriendlyGraphQLError(String message, List<SourceLocation> locations, ExecutionPath path) {
  7. this.message = message;
  8. this.locations = locations;
  9. this.path = path;
  10. }
  11. @Override
  12. public String getMessage() {
  13. return message;
  14. }
  15. @Override
  16. public List<SourceLocation> getLocations() {
  17. return locations;
  18. }
  19. @Override
  20. public ErrorClassification getErrorType() {
  21. return CustomErrorClassification.USER_FRIENDLY_ERROR;
  22. }
  23. @Override
  24. public List<Object> getPath() {
  25. return path.toList();
  26. }
  27. }
  1. public enum CustomErrorClassification implements ErrorClassification {
  2. USER_FRIENDLY_ERROR
  3. }

的数据
然后我创建了DataFetcherExceptionHandler来将其转换为适当的GraphQL响应

  1. /**
  2. * Converts exceptions into error response
  3. */
  4. public class GraphQLExceptionHandler implements DataFetcherExceptionHandler {
  5. private final DataFetcherExceptionHandler delegate = new SimpleDataFetcherExceptionHandler();
  6. @Override
  7. public DataFetcherExceptionHandlerResult onException(DataFetcherExceptionHandlerParameters handlerParameters) {
  8. // handle user friendly errors
  9. if (handlerParameters.getException() instanceof UserFriendlyException) {
  10. GraphQLError error = new UserFriendlyGraphQLError(
  11. handlerParameters.getException().getMessage(),
  12. List.of(handlerParameters.getSourceLocation()),
  13. handlerParameters.getPath());
  14. return DataFetcherExceptionHandlerResult.newResult().error(error).build();
  15. }
  16. // delegate to default handler otherwise
  17. return delegate.onException(handlerParameters);
  18. }
  19. }


最后在AsyncTransactionalExecutionStrategy中使用了它,还使用了@Transactional注解来允许惰性解析器。

  1. @Component
  2. public class AsyncTransactionalExecutionStrategy extends AsyncExecutionStrategy {
  3. public AsyncTransactionalExecutionStrategy() {
  4. super(new GraphQLExceptionHandler());
  5. }
  6. @Override
  7. @Transactional
  8. public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
  9. return super.execute(executionContext, parameters);
  10. }
  11. }


现在,如果你在某个地方throw new UserFriendlyException("Email already exists");,你最终会得到这样的好响应

  1. {
  2. "errors": [
  3. {
  4. "message": "Email already exists",
  5. "locations": [
  6. {
  7. "line": 2,
  8. "column": 3
  9. }
  10. ],
  11. "path": [
  12. "createUser"
  13. ],
  14. "extensions": {
  15. "classification": "USER_FRIENDLY_ERROR"
  16. }
  17. }
  18. ],
  19. "data": null
  20. }


给定分类USER_FRIENDLY_ERROR,如果您使UserFriendlyException消息用户友好,则可以直接将其显示给用户:)
但是,如果你在某个方法中使用throw new UserFriendlyException("Email already exists");注解@Transactional,你最终会得到空响应和HTTP 400状态。
@Transactional(propagation = Propagation.REQUIRES_NEW)添加到Mutation可解决此问题

  1. @Transactional(propagation = Propagation.REQUIRES_NEW)
  2. public class Mutation implements GraphQLMutationResolver {
  3. public User createUser(...) {
  4. ...
  5. }
  6. }

  • 请注意,这可能不是那么高性能的解决方案。不过,它可以满足一些较小的项目。*
展开查看全部
tzdcorbm

tzdcorbm6#

我假设,每当你获取一个 Show 对象时,你想要所有与 Show 对象相关的 Competition
默认情况下,实体中所有集合类型的获取类型为LAZY。您可以指定EAGER类型以确保hibernate获取集合。
Show 类中,可以将fetchType更改为EAGER

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

字符串

hpcdzsge

hpcdzsge7#

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

相关问题