The best way to fix the Hibernate MultipleBagFetchException
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
If you’ve been using Hibernate for some time, there is a good chance you bumped into a MultipleBagFetchException
issue:
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags
In this article, we are going to see the reason Hibernate throws the MultipleBagFetchException
as well as the best way to solve this issue.
Domain Model
Let’s consider that our application defines three entities: Post
, PostComment
, and Tag
, which are associated as in the following diagram:
What we are mostly interested in this article is that the Post
entity defines a bidirectional @OneToMany
association with the PostComment
child entity, as well as a unidirectional @ManyToMany
association with the Tag
entity.
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private List<PostComment> comments = new ArrayList<>(); @ManyToMany( cascade = { CascadeType.PERSIST, CascadeType.MERGE } ) @JoinTable( name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private List<Tag> tags = new ArrayList<>();
The reason why the
@ManyToMany
association cascades only thePERSIST
andMERGE
entity state transitions and not theREMOVE
one is because the other side is not a child entity.Since the
Tag
entity lifecycle is not tied to thePost
entity, cascadingREMOVE
or enabling theorphanRemoval
mechanism would be a mistake. For more details about this topic, check out this article.
Hibernate throwing MultipleBagFetchException
Now, if we want to fetch the Post
entities with the identifier values between 1 and 50, along with all their associated PostComment
and Tag
entities, we would write a query like the following one:
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();
However, when running the entity query above, Hibernate throws a MultipleBagFetchException
while compiling the JPQL query:
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 ]
So, no SQL query is executed by Hibernate. The reason why a MultipleBagFetchException
is thrown by Hibernate is that duplicates can occur, and the unordered List
, which is called a bag in Hibernate terminology, is not supposed to remove duplicates.
How NOT to “fix” the Hibernate MultipleBagFetchException
If you google the MultipleBagFetchException
, you are going to see many wrong answers, like this one on StackOverflow, which, surprisingly, has over 280 upvotes.
So simple, yet so wrong!
Using Set instead of List
So, let’s change the association collection type from List
to Set
:
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private Set<PostComment> comments = new HashSet<>(); @ManyToMany( cascade = { CascadeType.PERSIST, CascadeType.MERGE } ) @JoinTable( name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Set<Tag> tags = new HashSet<>();
And, now, when rerunning the previous entity query which fetched some Post
entities along with their comments
and tags
associations, we can see that no MultipleBagFetchException
is thrown.
However, this is SQL query that Hibernate executed for the aforementioned JPQL query:
SELECT p.id AS id1_0_0_, pc.id AS id1_1_1_, t.id AS id1_3_2_, p.title AS title2_0_0_, pc.post_id AS post_id3_1_1_, pc.review AS review2_1_1_, t.name AS name2_3_2_, pt.post_id AS post_id1_2_1__, pt.tag_id AS tag_id2_2_1__ FROM post p LEFT OUTER JOIN post_comment pc ON p.id = pc.post_id LEFT OUTER JOIN post_tag pt ON p.id = pt.post_id LEFT OUTER JOIN tag t ON pt.tag_id = t.id WHERE p.id BETWEEN 1 AND 50
So, what’s wrong with this SQL query?
The post
and post_comment
are associated via the post_id
Foreign Key column, so the join produces a result set containing all post
table rows with the Primary Key values between 1 and 50 along with their associated post_comment
table rows.
The post
and tag
tables are also associated via the post_id
and tag_id
post_tag
Foreign Key columns, so these two joins produce a result set containing all post
table rows with the Primary Key values between 1 and 50 along with their associated tag
table rows.
Now, to merge the two result sets, the database can only use a Cartesian Product, so the final result set contains 50 post
rows multiplied by the associated post_comment
and tag
table rows.
So, if we have 50 post
rows associated with 20 post_comment
and 10 tag
rows, the final result set will contain 10_000 records (e.g., 50 x 20 x 10), as illustrated by the following test case:
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(); assertEquals( POST_COUNT * POST_COMMENT_COUNT * TAG_COUNT, posts.size() );
That’s so terrible from a performance perspective!
If you want to see how you can fix the
MultipleBagFetchException
when using Spring Data JPA, then check out this article.
How to fix the Hibernate MultipleBagFetchException
To avoid a Cartesian Product, you can fetch at most one association at a time. So, instead of executing a single JPQL query that fetches two associations, we can execute two JPQL queries instead:
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(); assertEquals(POST_COUNT, posts.size()); for(Post post : posts) { assertEquals(POST_COMMENT_COUNT, post.getComments().size()); assertEquals(TAG_COUNT, post.getTags().size()); }
The first JPQL query defines the main filtering criteria and fetches the Post
entities along with the associated PostComment
records.
The
PASS_DISTINCT_THROUGH
query hint allows you to avoid passing the DISTINCT keyword to the SQL statement, and only use it to remove Java entity duplicates caused by the parent-child joined result set. For more details about thePASS_DISTINCT_THROUGH
query hint, check out this article.
Now, we have to fetch the Post
entities along with their associated Tag
entities, and, thanks to the Persistence Context, Hibernate will set the tags
collection of the previously fetched Post
entities.
Cool, right?
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
There are so many blog posts, videos, books, and forum answers, providing the wrong solution to the MultipleBagFetchException
Hibernate issues. All these resources tell you that using a Set
instead of a List
is the right way to avoid this exception.
However, the MultipleBagFetchException
tells you that a Cartesian Product might be generated, and, most of the time, that’s undesirable when fetching entities as it can lead to terrible data access performance issues.
The best way to fetch multiple entity collections with JPA and Hibernate is to load at most one collection at a time while relying on the Hibernate Persistence Context guarantee that only a single entity object can be loading at a time in a given JPA EntityManager
or Hibernate Session
.

Nice post as always! The code you provided should be executed in transactional scope right?
All your database-related service methods should run in a Transactional context, and this solution here is no different.
Thank you for the quick reply!
I used to exclude @Transactional annotation for performance purpose when working with Spring JPA. Is it bad habit? Should i use @Transactional annotation for all database-related service methods?
Of course, it’s a bad habit. Why did you assume it’s a performance improvement?
I didn’t mentioned that i only exclude @Transactional annotation when fetching query only.
If you call 3 read-only methods from the non-Tx service, you’d end up getting and releasing 2 DB connections from the pool. That’s not efficient.
Thanks!! Never thought about the connection pool.
Sounds like you haven’t read the amazing High-Performance Java Peristence.
If you did, you’d know about the connection pools and hundreds of other tips that can help you get the most out of your database.