考虑到我们有以下实体:
而且,您想要获取一些父 Post 实体以及所有关联的 comments 和 tags 集合。
如果您使用多个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。
这是一个糟糕的建议。不要那样做!
使用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();
在第一个 JPQL 查询中,distinct 不会转到 SQL 语句。这就是我们将PASS_DISTINCT_THROUGH JPA 查询提示设置为false 的原因。
DISTINCT 在 JPQL 中有两个含义,在这里,我们需要它在 Java 端而不是 SQL 端对 getResultList 返回的 Java 对象引用进行去重。
只要您使用JOIN FETCH 最多获取一个集合,就可以了。
通过使用多个查询,您将避免笛卡尔积,因为任何其他集合,但第一个是使用辅助查询获取的。
始终避免使用FetchType.EAGER 策略
如果您在为@OneToMany 或@ManyToMany 关联映射时使用FetchType.EAGER 策略,那么您很容易以MultipleBagFetchException 结束。
您最好从FetchType.EAGER 切换到Fetchype.LAZY,因为急切获取是一个糟糕的想法,可能会导致严重的应用程序性能问题。
结论
避免使用FetchType.EAGER 并且不要从List 切换到Set,因为这样做会使Hibernate 将MultipleBagFetchException 隐藏在地毯下。一次只获取一个集合,就可以了。
只要您使用与要初始化的集合相同数量的查询来执行此操作,就可以了。只是不要在循环中初始化集合,因为这会触发N+1 查询问题,这也对性能不利。