Hibernate throws MultipleBagFetchException - cannot simultaneously fetch multiple bags

前端 未结 15 1821
广开言路
广开言路 2020-11-22 12:49

Hibernate throws this exception during SessionFactory creation:

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple b

相关标签:
15条回答
  • 2020-11-22 12:59

    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.

    0 讨论(0)
  • 2020-11-22 13:00

    This question has been a recurring theme on both StackOverflow or the Hibernate forum, so I decided to turn the answer into an article as well.

    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 the PASS_DISTINCT_THROUGH JPA query hint to false.

    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. Check out this article for more details.

    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.

    0 讨论(0)
  • 2020-11-22 13:00

    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

    0 讨论(0)
  • 2020-11-22 13:03

    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.

    0 讨论(0)
  • 2020-11-22 13:03

    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.

    0 讨论(0)
  • 2020-11-22 13:04

    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

    0 讨论(0)
提交回复
热议问题