17

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

我正在使用: https ://github.com/graphql-java/graphql-spring-boot 和 https://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 的一些优势。我可能需要寻找解决方案的任何想法?谢谢!

4

6 回答 6

14

我解决了它,我想应该更仔细地阅读 graphql-java-tools 库的文档。除了GraphQLQueryResolver解决基本查询之外GraphQLResolver<T>,我的Show班级还需要一个,如下所示:

@Component
public class ShowResolver implements GraphQLResolver<Show> {
    @Autowired
    private CompetitionRepository competitionRepository;

    public List<Competition> competitions(Show show) {
        return ((List<Competition>)competitionRepository.findByShowId(show.getId()));
    }
}

这告诉库如何解析我的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 类型,如下所示:

@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();
}
于 2018-01-01T00:07:01.953 回答
14

我首选的解决方案是在 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();
  }

}
于 2018-08-24T07:20:10.990 回答
2

对于任何对接受的答案感到困惑的人,您需要更改 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将打开一个事务以获取节目列表,但一旦完成,它将关闭事务。

然后嵌套的 graphql 查询competitions将尝试调用从前一个查询中检索到的getCompetition()每个实例,这将抛出 a因为事务已关闭。 ShowLazyInitializationException

{
  shows {
    id
    name
    competitions {
      id
    }
  }
}

接受的答案本质上是绕过通过OneToMany关系检索竞争列表,而是在新事务中创建一个新查询,从而消除问题。

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

于 2018-07-01T10:36:49.253 回答
0

对我来说,使用AsyncTransactionalExecutionStrategy工作不正确,但有例外。例如惰性初始化或应用程序级异常触发事务到仅回滚状态。然后 Spring 事务机制在 strategy 的边界抛出了仅回滚事务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

于 2020-05-21T06:00:14.793 回答
-4

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

于 2018-05-17T15:37:27.273 回答
-4

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

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

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

@OneToMany(cascade=CascadeType.ALL,fetch=FetchType.EAGER)
private List<Competition> competition;
于 2017-12-31T07:19:13.587 回答