Hibernate throws MultipleBagFetchException - cannot simultaneously fetch multiple bags
Hibernate throws this exception during SessionFactory creation:
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
This is my test case:
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;
}
How about thi开发者_运维技巧s problem? What can I do?
EDIT
OK, the problem I have is that another "parent" entity is inside my parent, my real behavior is this:
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 doesn't like two collections with FetchType.EAGER
, but this seems to be a bug, I'm not doing unusual things...
Removing FetchType.EAGER
from Parent
or AnotherParent
solves the problem, but I need it, so real solution is to use @LazyCollection(LazyCollectionOption.FALSE)
instead of FetchType
(thanks to Bozho for the solution).
I think a newer version of hibernate (supporting JPA 2.0) should handle this. But otherwise you can work it around by annotating the collection fields with:
@LazyCollection(LazyCollectionOption.FALSE)
Remember to remove the fetchType
attribute from the @*ToMany
annotation.
But note that in most cases a Set<Child>
is more appropriate than List<Child>
, so unless you really need a List
- go for Set
But remind that with using sets you won't eliminate the underlaying Cartesian Product as described by Vlad Mihalcea in his answer!
Simply change from List
type to Set
type.
But remind that you won't eliminate the underlaying Cartesian Product as described by Vlad Mihalcea in his answer!
Add a Hibernate-specific @Fetch annotation to your code:
@OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
@Fetch(value = FetchMode.SUBSELECT)
private List<Child> childs;
This should fix the issue, related to Hibernate bug HHH-1718
Considering we have the following entities:
And, you want to fetch some parent Post
entities along with all the comments
and tags
collections.
If you are using more than one JOIN FETCH
directives:
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 will throw the infamous:
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 doesn't allow fetching more than one bag because that would generate a Cartesian product.
The worst "solution"
Now, you will find lots of answers, blog posts, videos, or other resources telling you to use a Set
instead of a List
for your collections.
That's terrible advice. Don't do that!
Using Sets
instead of Lists
will make the MultipleBagFetchException
go away, but the Cartesian Product will still be there, which is actually even worse, as you'll find out the performance issue long after you applied this "fix".
The proper solution
You can do the following trick:
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 thePASS_DISTINCT_THROUGH
JPA query hint tofalse
.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.
As long as you fetch at most one collection using JOIN FETCH
, you will be fine.
By using multiple queries, you will avoid the Cartesian Product since any other collection, but the first one is fetched using a secondary query.
There's more you could do
If you're using the FetchType.EAGER
strategy at mapping time for @OneToMany
or @ManyToMany
associations, then you could easily end up with a MultipleBagFetchException
.
You are better off switching from FetchType.EAGER
to Fetchype.LAZY
since eager fetching is a terrible idea that can lead to critical application performance issues.
Conclusion
Avoid FetchType.EAGER
and don't switch from List
to Set
just because doing so will make Hibernate hide the MultipleBagFetchException
under the carpet. Fetch just one collection at a time, and you'll be fine.
As long as you do it with the same number of queries as you have collections to initialize, you are fine. Just don't initialize the collections in a loop, as that will trigger N+1 query issues, which are also bad for performance.
After trying with every single option describe in this posts and others, I came to the conclusion that the the fix is a follows.
In every XToMany place @XXXToMany(mappedBy="parent", fetch=FetchType.EAGER)
and intermediately after
@Fetch(value = FetchMode.SUBSELECT)
This worked for me
To fix it simply take Set
in place of List
for your nested object.
@OneToMany
Set<Your_object> objectList;
and don't forget to use fetch=FetchType.EAGER
it will work.
There is one more concept CollectionId
in Hibernate if you want to stick with list only.
But remind that you won't eliminate the underlaying Cartesian Product as described by Vlad Mihalcea in his answer!
I found a good Blog post about the behaviour of Hibernate in this kind of object mappings: http://blog.eyallupu.com/2010/06/hibernate-exception-simultaneously.html
you can keep booth EAGER lists in JPA and add to at least one of them the JPA annotation @OrderColumn (with obviously the name of a field to be ordered). No need of specific hibernate annotations. But keep in mind it could create empty elements in the list if the chosen field does not have values starting from 0
[...]
@OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
@OrderColumn(name="orderIndex")
private List<Child> children;
[...]
in Children then you should add the orderIndex field
When you have too complex objects with saveral collection could not be good idea to have all of them with EAGER fetchType, better use LAZY and when you really need to load the collections use: Hibernate.initialize(parent.child)
to fetch the data.
We tried Set instead of List and it is a nightmare: when you add two new objects, equals() and hashCode() fail to distinguish both of them ! Because they don't have any id.
typical tools like Eclipse generate that kind of code from Database tables:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
You may also read this article that explains properly how messed up JPA/Hibernate is. After reading this, I think this is the last time I use any ORM in my life.
I've also encounter Domain Driven Design guys that basically say ORM are a terrible thing.
For me, the problem was having nested EAGER fetches.
One solution is to set the nested fields to LAZY and use Hibernate.initialize() to load the nested field(s):
x = session.get(ClassName.class, id);
Hibernate.initialize(x.getNestedField());
At my end, this happened when I had multiple collections with FetchType.EAGER, like this:
@ManyToMany(fetch = FetchType.EAGER, targetEntity = className.class)
@JoinColumn(name = "myClass_id")
@JsonView(SerializationView.Summary.class)
private Collection<Model> ModelObjects;
Additionally, the collections were joining on the same column.
To solve this issue, I changed one of the collections to FetchType.LAZY since it was okay for my use-case.
Goodluck! ~J
You can also try to make fetch=FetchType.LAZY and just add @Transactional(readOnly = true) to method where you get child
Commenting both Fetch
and LazyCollection
sometimes helps to run project.
@Fetch(FetchMode.JOIN)
@LazyCollection(LazyCollectionOption.FALSE)
One good thing about @LazyCollection(LazyCollectionOption.FALSE)
is that several fields with this annotation can coexist while FetchType.EAGER
cannot, even in the situations where such coexistence is legit.
For example, an Order
may have a list of OrderGroup
(a short one) as well as a list of Promotions
(also short). @LazyCollection(LazyCollectionOption.FALSE)
can be used on both without causing LazyInitializationException
neither MultipleBagFetchException
.
In my case @Fetch
did solve my problem of MultipleBacFetchException
but then causes LazyInitializationException
, the infamous no Session
error.
I solved by annotating:
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
Ok so here's my 2 cents. I had the Fetch Lazy annotations in my Entity but I also duplicated the fetch lazy in the session bean thus causing a multiple bag issue. So I just removed the lines in my SessionBean
criteria.createAlias("FIELD", "ALIAS", JoinType.LEFT_OUTER_JOIN); //REMOVED
And I used Hibernate.initialize on the list I wanted to retrieve after the List parent = criteria.list was called. Hibernate.initialize(parent.getChildList());
You could use a new annotation to solve this:
@XXXToXXX(targetEntity = XXXX.class, fetch = FetchType.LAZY)
In fact, fetch's default value is FetchType.LAZY too.
精彩评论