Hibernate抛出MultipleBagFetchException异常 - 无法同时获取多个bag

607

Hibernate在创建SessionFactory时会抛出此异常:

org.hibernate.loader.MultipleBagFetchException:不能同时获取多个bags

这是我的测试案例:

Parent.java

@Entity
public Parent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 // @IndexColumn(name="INDEX_COL") if I had this the problem solve but I retrieve more children than I have, one child is null.
 private List<Child> children;

}

Child.java

@Entity
public Child {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @ManyToOne
 private Parent parent;

}

这个问题怎么样?我该怎么办?


编辑

好的,我的问题是我的一个“父级”实体在我的父级内部,我的真正行为是这样的:

Parent.java

@Entity
public Parent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @ManyToOne
 private AnotherParent anotherParent;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 private List<Child> children;

}

AnotherParent.java

@Entity
public AnotherParent {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private Long id;

 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 private List<AnotherChild> anotherChildren;

}

Hibernate不喜欢具有FetchType.EAGER的两个集合,但这似乎是一个bug,我没有做什么异常的事情...

ParentAnotherParent中删除FetchType.EAGER可以解决问题,但我需要它,因此真正的解决方案是使用@LazyCollection(LazyCollectionOption.FALSE)而不是FetchType(感谢Bozho提供的解决方案)。


2
我想问一下,您希望生成什么SQL查询来同时检索两个不同的集合?能够实现这些的SQL类型要么需要进行笛卡尔积(可能非常低效),要么需要对不相交的列进行UNION(也很丑陋)。可以推测,在SQL中无法以干净和高效的方式实现这一点影响了API设计。 - Thomas W
@ThomasW 这些是它应该生成的 SQL 查询语句:select * from master; select * from child1 where master_id = :master_id; select * from child2 where master_id = :master_id - nurettin
2
如果您有多个已定义fetchTypeList<child>,则可能会出现类似的错误,这些List<child>多个List<child> - Big Zed
18个回答

687
我认为一个支持JPA 2.0的更新版本的Hibernate应该可以处理这个问题。但是如果不行的话,你可以通过在集合字段上添加注解来解决这个问题:
@LazyCollection(LazyCollectionOption.FALSE)

请记住从@*ToMany注解中删除fetchType属性。
但请注意,大多数情况下,Set<Child>List<Child>更合适,所以除非真正需要List,否则请使用Set

谨慎使用

请记住,使用Set 不会消除笛卡尔积,如Vlad Mihalcea 在他的回答中所描述的!

6
奇怪,这对我有用。你从 @*ToMany 中删除了 fetchType 吗? - Bozho
118
问题在于JPA注解被解析时不允许加载超过两个集合(collection)的急加载。但是,Hibernate特定的注解允许这样做。 - Bozho
21
需要超过1个EAGER似乎完全合理。这个限制只是JPA的疏忽吗?当有多个EAGER时,我应该注意哪些问题? - AR3Y35
9
问题在于,Hibernate 无法通过一个查询获取两个集合。因此,当您查询父实体时,每个结果需要额外进行两次查询,这通常不是您想要的。 - Bozho
12
能否解释一下为什么这样做可以解决问题,那就太好了。 - Ben
显示剩余10条评论

328

只需将 List 类型更改为 Set 类型。

请谨慎使用

不建议使用此解决方案,因为它无法消除由Vlad Mihalcea在他的回答中描述的基础笛卡尔积


44
列表和集合不是同一种东西:集合没有保留顺序。 - Matteo
19
LinkedHashSet 保留顺序。 - egallardo
18
这是一个重要的区别,仔细想想,完全正确。在数据库中通过外键实现的典型多对一关系实际上不是列表,而是集合,因为顺序没有保留。因此,使用集合更为合适。我认为这就是Hibernate的区别所在,尽管我不知道为什么。 - fool4jesus
30
我喜欢这个答案,但是关键问题是:为什么?为什么使用Set不会显示异常?谢谢。 - Hinotori
3
当我使用列表时,我有一个完全有效的用例,但遇到了同样的异常问题。我按照Ahmad的建议更改了代码,现在一切都“OK”了。 :) - bigMC28
显示剩余5条评论

206
考虑到我们有以下实体:

enter image description here

而且,你想获取一些父级Post实体,以及所有的commentstags集合。
如果你使用了多个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会抛出臭名昭著的异常:
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不允许获取多个bag,因为这会产生笛卡尔积。
最糟糕的“解决方案”
现在,你会发现很多答案、博客文章、视频或其他资源告诉你在集合中使用Set而不是List。
这是一个糟糕的建议。不要这样做!
使用Set而不是List会消除MultipleBagFetchException,但笛卡尔积仍然存在,这实际上更糟糕,因为你会在应用这个“修复”之后很久才发现性能问题。
正确的Hibernate 6解决方案
如果你正在使用Hibernate 6,那么你可以像这样解决这个问题:
List<Post> posts = entityManager.createQuery("""
    select p
    from Post p
    left join fetch p.comments
    where p.id between :minId and :maxId
    """, Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.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)
.getResultList();

只要在每个查询中最多使用一个JOIN FETCH来获取集合,你就没问题。
通过使用多个查询,你可以避免笛卡尔积,因为除了第一个集合外,其他集合都是通过次要查询来获取的。

Hibernate 5的正确解决方案

你可以使用以下技巧:
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的原因。
在JPQL中,distinct有两个含义,而在这里,我们需要它来对getResultList在Java端返回的Java对象引用进行去重,而不是在SQL端进行去重。
通过使用多个查询,您可以避免笛卡尔积,因为除了第一个集合外,其他集合都是使用辅助查询获取的。

还有更多可以做的

如果您在映射时对@OneToMany@ManyToMany关联使用了FetchType.EAGER策略,那么很容易出现MultipleBagFetchException
最好将FetchType.EAGER切换为Fetchype.LAZY,因为急切获取是一个糟糕的想法,可能会导致关键应用程序性能问题。

结论

避免使用FetchType.EAGER,并且不要仅仅因为这样做会让Hibernate将MultipleBagFetchException隐藏起来而从List切换到Set。一次只获取一个集合,这样就没问题了。
只要你使用与要初始化的集合数量相同的查询数量,就没问题。只是不要在循环中初始化集合,因为这会触发N+1查询问题,这对性能也不好。

3
不可以。从 SQL 的角度来看,你不能连接多个一对多的关联而不生成笛卡尔积。 - Vlad Mihalcea
6
你应该在调用Spring Data Jpa Repository的服务方法上始终添加@Transactional。不这样做是一个可怕的错误。 - Vlad Mihalcea
2
我试图将此与带有@NamedEntityGraph注释的实体类一起使用。除非我删除图形逻辑,否则它不起作用。实体图技术有什么优点吗? - Joseph Gagnon
2
这要视情况而定。对于可嵌入的情况,集合(Sets)更好。对于双向一对多关联,无所谓,但列表(List)更加通用。对于多对多关系,集合(Sets)更好。您可以在我的《高性能Java持久化》书中找到详细的解释。 - Vlad Mihalcea
2
@JayD 我会在未来的博客中写到这个。 - Vlad Mihalcea
显示剩余35条评论

185

在你的代码中添加一个特定于Hibernate的@Fetch注解:

@OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
@Fetch(value = FetchMode.SUBSELECT)
private List<Child> childs;

这应该能解决与Hibernate bug HHH-1718相关的问题。


6
@DaveRlz,为什么使用subSelect可以解决这个问题?我尝试了你的解决方案,它有效了,但是不知道是如何使用subSelect解决的。 - HakunaMatata
4
如果没有必要使用 Set,那么这就是最佳答案。使用 Set 的单个 OneToMany 关系会导致 1+<# relationships> 次查询,而使用 FetchMode.SUBSELECT 则只需要进行 1+1 次查询。此外,在已接受的答案中使用注释(LazyCollectionOption.FALSE)将导致执行更多的查询。 - mstrthealias
1
FetchType.EAGER不是一个合适的解决方案。 需要使用Hibernate Fetch Profiles来解决它。 - Milinda Bandara
3
另外两个最佳答案没有解决我的问题,这个解决了。谢谢! - Blindworks
5
有人知道为什么使用子查询(SUBSELECT)可以修复它,但联接(JOIN)则不行吗? - Innokenty
显示剩余6条评论

48

在尝试了这篇文章和其他文章中描述的每个选项之后,我得出结论,解决方法如下。

在每个XToMany位置添加@XXXToMany(mappedBy="parent", fetch=FetchType.EAGER),然后在其中间添加

@Fetch(value = FetchMode.SUBSELECT)

这对我起作用了


7
添加@Fetch(value = FetchMode.SUBSELECT)就足够了。 - user2601995
1
这是一个仅使用Hibernate的解决方案。如果您正在使用共享JPA库怎么办? - Michel
5
我确定你不是有意的,但是DaveRlz已经在3年前写过相同的话了。 - phil294
1
这可能是上面答案的重复:https://stackoverflow.com/a/8309458/3451846 - undefined

17

要修复它,只需将嵌套对象中的List替换为Set

@OneToMany
Set<Your_object> objectList;

不要忘记使用 fetch=FetchType.EAGER,它会起作用。

如果你只想使用列表,Hibernate 还有一个名为 CollectionId 的概念。

谨慎使用

请记住,你不会消除Vlad Mihalcea 在他的回答中所描述的底层笛卡尔积


1
你提出的两个建议在性能方面都非常糟糕。 - Carlos López Marí

6

您可以在JPA中保持两个EAGER列表,并向其中至少一个添加JPA注解@OrderColumn(显然需要指定要排序的字段名称)。不需要特定的Hibernate注解。但请记住,如果所选字段没有从0开始的值,则可能会创建空元素。

 [...]
 @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
 @OrderColumn(name="orderIndex")
 private List<Child> children;
 [...]

如果涉及到儿童,则需要添加orderIndex字段。


3

当您拥有包含多个集合的过于复杂的对象时,将所有集合都使用EAGER fetchType可能不是一个好主意,最好使用LAZY。当您真正需要加载集合时,请使用Hibernate.initialize(parent.child)来获取数据。


2

我们尝试使用Set代替List,结果是一场噩梦:当您添加两个新对象时,equals()和hashCode()无法区分它们!因为它们没有任何ID。

像Eclipse这样的典型工具会从数据库表生成此类代码:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((id == null) ? 0 : id.hashCode());
    return result;
}

您还可以阅读这篇文章,它详细解释了JPA/Hibernate有多么混乱。阅读后,我认为这是我一生中最后一次使用任何ORM。

我也遇到了领域驱动设计的人,他们基本上说ORM是一件可怕的事情。


1
你也可以尝试将fetch设置为FetchType.LAZY,并在获取子项的方法上添加@Transactional(readOnly = true)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接