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

2022-08-31 19:53:46

我有以下实体:

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查询加入和加入的类别。我写了如下查询:topicsposts

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

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

是否可以在一个查询中执行此操作?


答案 1

考虑到我们有以下实体:

JPA entity domain model

并且,您希望获取一些父实体以及所有关联的和集合。Postcommentstags

如果使用多个指令: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抛出这个异常的原因是它不允许获取多个袋子,因为这会生成笛卡尔积。

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

现在,您会发现许多答案,博客文章,视频或其他资源,告诉您对集合使用a而不是a。SetList

这是一个可怕的建议。别这样!

使用而不是会使消失,但笛卡尔积仍然存在,这实际上更糟,因为在应用此“修复”后很久您就会发现性能问题。SetsListsMultipleBagFetchException

正确的解决方案

您可以执行以下技巧:

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

在第一个 JPQL 查询中,不转到 SQL 语句。这就是我们将 JPA 查询提示设置为 .distinctPASS_DISTINCT_THROUGHfalse

DISTINCT在JPQL中有两个含义,在这里,我们需要它来重复 Java 端返回的 Java 对象引用,而不是 SQL 端。getResultList

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

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

始终避免策略FetchType.EAGER

如果您在映射或关联时使用该策略,则很容易得到 .FetchType.EAGER@OneToMany@ManyToManyMultipleBagFetchException

您最好从 切换到 ,因为急切的获取是一个可怕的想法,可能会导致关键的应用程序性能问题。FetchType.EAGERFetchype.LAZY

结论

避免并且不要仅仅因为这样做会使Hibernate隐藏在地毯下而切换到。一次只获取一个集合,你就会没事的。FetchType.EAGERListSetMultipleBagFetchException

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


答案 2

下面是复杂连接和多重构件的工作示例:

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

推荐