如何在一个 JPQL 查询中使用多个 JOIN FETCH

How to use multiple JOIN FETCH in one JPQL query

我有以下实体:

public class Category {
   private Integer id;

   @OneToMany(mappedBy = "parent")
   private List<Topic> topics;
}

public class Topic {
   private Integer id;

   @OneToMany(mappedBy = "parent")
   private List<Posts> posts;

   @ManyToOne
   @JoinColumn(name = "id")
   private Category parent;
}

public class Post {
   private Integer id;

   @ManyToOne
   @JoinColumn(name = "id")
   private Topic parent;
   /* Post fields */
}

我想使用 JPQL 查询获取所有加入 topics 和加入 posts 的类别。我写了如下查询:

SELECT c FROM Category c
JOIN FETCH c.topics t
JOIN FETCH t.posts p WHERE 

但是我得到了错误

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags

我找到了有关此错误的文章,但这些文章仅描述了在一个实体中有两个要加入的集合的情况。我的问题有点不同,我不知道如何解决。

是否可以一次查询完成?

考虑到我们有以下实体:

并且,您想获取一些 parent Post 实体以及所有关联的 commentstags collection 实体。

如果您使用多个 JOIN FETCH 指令:

List<Post> posts = entityManager.createQuery("""
    select p
    from Post p
    left join fetch p.comments
    left join fetch p.tags
    where p.id between :minId and :maxId
    """, Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.getResultList();

Hibernate 将抛出 MultipleBagFetchException:

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags [
  com.vladmihalcea.book.hpjp.hibernate.fetching.Post.comments,
  com.vladmihalcea.book.hpjp.hibernate.fetching.Post.tags
]

Hibernate 抛出此异常的原因是它不允许获取多个包,因为那样会生成笛卡尔积。

其他人可能会试图推销给你的最糟糕的“解决方案”

现在,您会发现很多答案、博客文章、视频或其他资源告诉您使用 Set 而不是 List 作为 collection。

这是糟糕的建议。不要那样做!

使用 Sets 而不是 Lists 会使 MultipleBagFetchException 消失,但笛卡尔积仍会存在,这实际上更糟,你会发现应用此“修复”很久之后的性能问题。

正确的解决方案

您可以使用以下技巧:

List<Post> posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.comments
    where p.id between :minId and :maxId
    """, Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

posts = entityManager.createQuery("""
    select distinct p
    from Post p
    left join fetch p.tags t
    where p in :posts
    """, Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

In the first JPQL query, distinct DOES NOT go to the SQL statement. That's why we set the PASS_DISTINCT_THROUGH JPA query hint to false.

DISTINCT has two meanings in JPQL, and here, we need it to deduplicate the Java object references returned by getResultList on the Java side, not the SQL side.

只要使用 JOIN FETCH 最多获取一个 collection,就可以了。

通过使用多个查询,您将避免使用笛卡尔积,因为任何其他 collection 但第一个是使用辅助查询获取的。

始终避免 FetchType.EAGER 策略

如果您在 @OneToMany@ManyToMany 关联的映射时使用 FetchType.EAGER 策略,那么您很容易得到 MultipleBagFetchException

您最好从 FetchType.EAGER 切换到 Fetchype.LAZY,因为急切获取是一个糟糕的想法,可能会导致严重的应用程序性能问题。

结论

避免使用 FetchType.EAGER 并且不要从 List 切换到 Set,因为这样做会使 Hibernate 将 MultipleBagFetchException 隐藏在地毯下。一次只取一个 collection,你会没事的。

只要您使用与要初始化的 collection 相同数量的查询来执行此操作,就可以了。只是不要在循环中初始化 collection,因为这会触发 N+1 查询问题,这对性能也很不利。

这是一个复杂连接和多重条件的工作示例:

    String query_findByProductDepartmentHospital = "select location from ProductInstallLocation location "
            + " join location.product prod " + " join location.department dep "
            + " join location.department.hospital hos " + " where  prod.name = :product "
            + " and dep.name.name = :department " + " and hos.name = :hospital ";

    @Query(query_findByProductDepartmentHospital)
    ProductInstallLocation findByProductDepartmentHospital(@Param("product") String productName,@Param("department") String departName, @Param("hospital") String hospitalName);

解决方法是同时使用@Query 和@EntityGraph,就像这里提到的那样