A Guide to MultipleBagFetchException in Hibernate – Hibernate中的MultipleBagFetchException指南

最后修改: 2020年 12月 5日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll talk about the MultipleBagFetchException. We’ll begin with the necessary terms to understand, and then we’ll explore some workarounds until we reach the ideal solution.

在本教程中,我们将讨论MultipleBagFetchException我们将从需要理解的必要术语开始,然后我们将探索一些变通方法,直到我们达到理想的解决方案。

We’ll create a simple music app’s domain to demonstrate each of the solutions.

我们将创建一个简单的音乐应用程序的领域,以展示每个解决方案。

2. What Is a Bag in Hibernate?

2. What Is a Bag in Hibernate?/span>

A Bag, similar to a List, is a collection that can contain duplicate elements. However, it is not in order. Moreover, a Bag is a Hibernate term and isn’t part of the Java Collections Framework.

Bag,类似于List,是一个可以包含重复元素的集合。此外,Bag是一个Hibernate术语,并不是Java集合框架的一部分。

Given the earlier definition, it’s worth highlighting that both List and Bag uses java.util.List. Although in Hibernate, both are treated differently. To differentiate a Bag from a List, let’s look at it in actual code.

鉴于前面的定义,值得强调的是,List和Bag都使用java.util.List。尽管在Hibernate中,两者的处理方式不同。为了区分Bag和List,让我们在实际的代码中看一下。

A Bag:

一个袋子。

// @ any collection mapping annotation
private List<T> collection;

A List:

一个清单

// @ any collection mapping annotation
@OrderColumn(name = "position")
private List<T> collection;

3. Cause of MultipleBagFetchException

3.造成MultipleBagFetchException的原因

Fetching two or more Bags at the same time on an Entity could form a Cartesian Product. Since a Bag doesn’t have an order, Hibernate would not be able to map the right columns to the right entities. Hence, in this case, it throws a MultipleBagFetchException.

在一个实体上同时获取两个或多个Bag,可能会形成一个笛卡尔产品。由于Bag没有顺序,Hibernate将无法将正确的列映射到正确的实体。因此,在这种情况下,它会抛出一个MultipleBagFetchException

Let’s have some concrete examples that lead to MultipleBagFetchException.

让我们来看看一些导致MultipleBagFetchException.的具体例子。

For the first example, let’s try to create a simple entity that has 2 bags and both with eager fetch type. An Artist might be a good example. It can have a collection of songs and offers.

对于第一个例子,让我们试着创建一个简单的实体,它有2个包,并且都有eager fetch类型。一个艺术家可能是一个好例子。它可以有一个歌曲报价的集合。

Given that, let’s create the Artist entity:

鉴于此,让我们创建Artist实体。

@Entity
class Artist {

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

    private String name;

    @OneToMany(mappedBy = "artist", fetch = FetchType.EAGER)
    private List<Song> songs;

    @OneToMany(mappedBy = "artist", fetch = FetchType.EAGER)
    private List<Offer> offers;

    // constructor, equals, hashCode
}

If we try to run a test, we’ll encounter a MultipleBagFetchException immediately, and it won’t be able to build Hibernate SessionFactory. Having that said, let’s not do this.

如果我们试图运行一个测试,我们将立即遇到一个MultipleBagFetchException,它将无法构建Hibernate SessionFactory既然如此,我们就不要这样做。

Instead, let’s convert one or both of the collections’ fetch type to lazy:

相反,让我们把一个或两个集合的获取类型转换为懒惰。

@OneToMany(mappedBy = "artist")
private List<Song> songs;

@OneToMany(mappedBy = "artist")
private List<Offer> offers;

Now, we’ll be able to create and run a test. Although, if we try to fetch both of these bag collections at the same time, it would still lead to MultipleBagFetchException.

现在,我们就可以创建并运行一个测试。尽管如此,如果我们试图同时获取这两个包集合,还是会导致MultipleBagFetchException

4. Simulate a MultipleBagFetchException

4.模拟一个MultipleBagFetchException

In the previous section, we’ve seen the causes of MultipleBagFetchException. Here, let’s verify those claims by creating an integration test.

在上一节中,我们已经看到了MultipleBagFetchException的原因。在这里,让我们通过创建一个集成测试来验证这些说法。

For simplicity, let’s use the Artist entity that we’ve previously created.

为了简单起见,让我们使用我们之前创建的艺术家实体。

Now, let’s create the integration test, and let’s try to fetch both songs and offers at the same time using JPQL:

现在,让我们创建集成测试,并尝试使用JPQL同时获取歌曲报价

@Test
public void whenFetchingMoreThanOneBag_thenThrowAnException() {
    IllegalArgumentException exception =
      assertThrows(IllegalArgumentException.class, () -> {
        String jpql = "SELECT artist FROM Artist artist "
          + "JOIN FETCH artist.songs "
          + "JOIN FETCH artist.offers ";

        entityManager.createQuery(jpql);
    });

    final String expectedMessagePart = "MultipleBagFetchException";
    final String actualMessage = exception.getMessage();

    assertTrue(actualMessage.contains(expectedMessagePart));
}

From the assertion, we have encountered an IllegalArgumentException, which has a root cause of MultipleBagFetchException.

从断言来看,我们遇到了一个IllegalArgumentException它的根本原因是MultipleBagFetchException

5. Domain Model

5.领域模型

Before proceeding to possible solutions, let’s look at the necessary domain models, which we’ll use as a reference later on.

在继续讨论可能的解决方案之前,让我们看看必要的领域模型,我们以后会把这些模型作为参考。

Suppose we’re dealing with a music app’s domain. Given that, let’s narrow our focus toward certain entities: Album, Artist, and User. 

假设我们要处理的是一个音乐应用程序的领域。鉴于此,让我们把重点缩小到某些实体上。专辑、艺术家、用户。

We’ve already seen the Artist entity, so let’s proceed with the other two entities instead.

我们已经看到了Artist实体,所以让我们改用其他两个实体来进行。

First, let’s look at the Album entity:

首先,让我们看一下Album实体。

@Entity
class Album {

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

    private String name;

    @OneToMany(mappedBy = "album")
    private List<Song> songs;

    @ManyToMany(mappedBy = "followingAlbums")
    private Set<Follower> followers;

    // constructor, equals, hashCode

}

An Album has a collection of songs, and at the same time, could have a set of followers. 

一张专辑有一组歌曲,同时,可以有一组追随者

Next, here’s the User entity:

接下来,这里是User实体。

@Entity
class User {

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

    private String name;

    @OneToMany(mappedBy = "createdBy", cascade = CascadeType.PERSIST)
    private List<Playlist> playlists;

    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
    @OrderColumn(name = "arrangement_index")
    private List<FavoriteSong> favoriteSongs;
    
    // constructor, equals, hashCode
}

A User can create many playlists. Additionally, a User has a separate List for favoriteSongs wherein its order is based on the arrangement index.

一个用户可以创建许多播放列表。此外,一个用户有一个单独的列表,用于喜爱的歌曲,其顺序是基于排列索引。

6. Workaround: Using a Set in a Single JPQL Query

6.解决办法 在一个单一的JPQL查询中使用Set

Before anything else, let’s emphasize that this approach would generate a cartesian product, which makes this a mere workaround. It’s because we’ll be fetching two collections simultaneously in a single JPQL query. In contrast, there’s nothing wrong with using a Set. It is the appropriate choice if we don’t need our collection to have an order or any duplicated elements.

在这之前,让我们强调一下,这种方法会产生一个笛卡尔积,这使得这只是一个变通方法。 这是因为我们将在一次JPQL查询中同时获取两个集合。相比之下,使用Set并无不妥。如果我们不需要我们的集合有一个顺序或任何重复的元素,它就是合适的选择。

To demonstrate this approach, let’s reference the Album entity from our domain model. 

为了演示这种方法,让我们引用我们领域模型中的Album实体。

An Album entity has two collections: songs and followers. The collection of songs is of type bag. However, for the followers, we’re using a Set. Having that said, we won’t encounter a MultipleBagFetchException even if we try to fetch both collections at the same time.

一个专辑实体有两个集合。歌曲追随者songs的集合是bag类型。然而,对于followers,我们使用的是Set。尽管如此,我们不会遇到MultipleBagFetchException 即使我们试图同时获取两个集合。

Using an integration test, let’s try to retrieve an Album by its id while fetching both of its collections in a single JPQL query:

使用一个集成测试,让我们尝试通过其id来检索Album,同时在一个JPQL查询中获取其两个集合。

@Test
public void whenFetchingOneBagAndSet_thenRetrieveSuccess() {
    String jpql = "SELECT DISTINCT album FROM Album album "
      + "LEFT JOIN FETCH album.songs "
      + "LEFT JOIN FETCH album.followers "
      + "WHERE album.id = 1";

    Query query = entityManager.createQuery(jpql)
      .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false);

    assertEquals(1, query.getResultList().size());
}

As we can see, we have successfully retrieved an Album. It’s because only the list of songs is a Bag. On the other hand, the collection of followers is a Set.

我们可以看到,我们已经成功地检索到了一个Album。这是因为只有歌曲的列表是一个Bag。另一方面,followers的集合是一个Set

On a side note, it’s worth highlighting that we’re making use of QueryHints.HINT_PASS_DISTINCT_THROUGH. Since we’re using an entity JPQL query, it prevents the DISTINCT keyword from being included in the actual SQL query. Thus, we’ll use this query hint for the remaining approaches as well. 

另外,值得强调的是,我们正在利用QueryHints.HINT_PASS_DISTINCT_THROUGH。由于我们使用的是实体JPQL查询,它可以防止DISTINCT关键字被包含在实际SQL查询中。因此,我们将在其余的方法中也使用这个查询提示。

7. Workaround: Using a List in a Single JPQL Query

7.解决办法 在一个单一的JPQL查询中使用List

Similar to the previous section, this would also generate a cartesian product, which could lead to performance issues. Again, there’s nothing wrong with using a List, Set, or Bag for the data type. The purpose of this section is to demonstrate further that Hibernate can fetch collections simultaneously given there’s no more than one of type Bag.

与上一节类似,这也会生成一个笛卡尔产品,这可能会导致性能问题。同样,使用List、Set,或Bag作为数据类型并没有错。本节的目的是进一步证明,鉴于Bag类型的集合不超过一个,Hibernate可以同时抓取集合。

For this approach, let’s use the User entity from our domain model.

对于这种方法,让我们使用我们领域模型中的User实体。

As mentioned earlier, a User has two collections: playlists and favoriteSongs. The playlists have no defined order, making it a bag collection. However, for the List of favoriteSongs, its order depends on how the User arranges it. If we look closely at the FavoriteSong entity, the arrangementIndex property made it possible to do so.

如前所述,一个用户有两个集合。播放列表喜爱的歌曲播放列表没有确定的顺序,使其成为一个袋状集合。然而,对于ListfavoriteSongs,其顺序取决于User如何安排。如果我们仔细观察FavoriteSong实体,arrangementIndex属性使我们有可能这样做。

Again, using a single JPQL query, let’s try to verify if we’ll be able to retrieve all the users while fetching both collections of playlists and favoriteSongs at the same time.

再次,使用单一的JPQL查询。让我们尝试验证一下,我们是否能够在同时获取playlistsfavoriteSongs两个集合的同时,检索到所有用户。

To demonstrate, let’s create an integration test:

为了演示,让我们创建一个集成测试:

@Test
public void whenFetchingOneBagAndOneList_thenRetrieveSuccess() {
    String jpql = "SELECT DISTINCT user FROM User user "
      + "LEFT JOIN FETCH user.playlists "
      + "LEFT JOIN FETCH user.favoriteSongs ";

    List<User> users = entityManager.createQuery(jpql, User.class)
      .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
      .getResultList();

    assertEquals(3, users.size());
}

From the assertion, we can see that we have successfully retrieved all users. Moreover, we didn’t encounter a MultipleBagFetchException. It’s because even though we’re fetching two collections at the same time, only the playlists is a bag collection.

从断言中,我们可以看到我们已经成功地检索了所有用户。此外,我们并没有遇到MultipleBagFetchException。这是因为即使我们同时获取两个集合,也只有playlists是一个袋子集合。

8. Ideal Solution: Using Multiple Queries

8.理想的解决方案 使用多个查询

We’ve seen from the previous workarounds the use of a single JPQL query for the simultaneous retrieval of collections. Unfortunately, it generates a cartesian product. We know that it’s not ideal. So here, let’s solve the MultipleBagFetchException without having to sacrifice performance.

我们已经从前面的解决方法中看到了使用一个JPQL查询来同时检索集合的情况。不幸的是,它生成了一个笛卡尔乘积。我们知道,这并不理想。所以在这里,让我们解决MultipleBagFetchException而不必牺牲性能。

Suppose we’re dealing with an entity that has more than one bag collection. In our case, it is the Artist entity. It has two bag collections: songs and offers.

假设我们要处理的实体有一个以上的包集合。在我们的例子中,它是Artist实体。它有两个包集合。songsoffers

Given this situation, we won’t even be able to fetch both collections at the same time using a single JPQL query. Doing so will lead to a MultipleBagFetchException. Instead, let’s split it into two JPQL queries.

鉴于这种情况,我们甚至不能用一个JPQL查询同时获取两个集合。这样做将导致一个MultipleBagFetchException。相反,让我们把它分成两个JPQL查询。

With this approach, we’re expecting to fetch both bag collections successfully, one at a time.

通过这种方法,我们希望能成功地获取两个袋子的集合,一次一个。

Again, for the last time, let’s quickly create an integration test for the retrieval of all artists:

再一次,最后一次,让我们快速创建一个检索所有艺术家的集成测试。

@Test
public void whenUsingMultipleQueries_thenRetrieveSuccess() {
    String jpql = "SELECT DISTINCT artist FROM Artist artist "
      + "LEFT JOIN FETCH artist.songs ";

    List<Artist> artists = entityManager.createQuery(jpql, Artist.class)
      .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
      .getResultList();

    jpql = "SELECT DISTINCT artist FROM Artist artist "
      + "LEFT JOIN FETCH artist.offers "
      + "WHERE artist IN :artists ";

    artists = entityManager.createQuery(jpql, Artist.class)
      .setParameter("artists", artists)
      .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
      .getResultList();

    assertEquals(2, artists.size());
}

From the test, we first retrieved all artists while fetching its collection of songs.

从测试中,我们首先检索了所有艺术家,同时获取其歌曲集

Then, we created another query to fetch the artists’ offers.

然后,我们创建了另一个查询来获取艺术家的offers

Using this approach, we avoided the MultipleBagFetchException as well as the formation of a cartesian product.

使用这种方法,我们避免了MultipleBagFetchException以及cartesian product的形成。

9. Conclusion

9.结语

In this article, we’ve explored MultipleBagFetchException in detail. We discussed the necessary vocabulary and the causes of this exception. We then simulated it. After that, we talked about a simple music app’s domain to have different scenarios for each of our workarounds and ideal solution. Lastly, we set up several integration tests to verify each of the approaches.

在这篇文章中,我们已经详细探讨了MultipleBagFetchException。我们讨论了必要的词汇和这个异常的原因。然后我们模拟了它。之后,我们谈到了一个简单的音乐应用程序的领域,以便为我们的每一个解决方法和理想的解决方案有不同的场景。最后,我们设置了几个集成测试来验证每一种方法。

As always, the full source code of the article is available over on GitHub.

一如既往,该文章的完整源代码可在GitHub上获得