1. 概要

このチュートリアルでは、 MultipleBagFetchExceptionについて説明します。理解するために必要な用語から始めて、理想的な解決策に到達するまでいくつかの回避策を検討します。

それぞれのソリューションを示すために、簡単な音楽アプリのドメインを作成します。

2. Hibernateのバッグとは何ですか?

リストと同様に、バッグは重複する要素を含むことができるコレクションです。 しかし、それは順調ではありません。 また、バッグは Hibernate 用語であり、Javaコレクションフレームワークの一部ではありません。

以前の定義を考えると、ListとBagの両方がjava.util.Listを使用していることを強調する価値があります。 Hibernateでは、どちらも異なる方法で処理されます。 バッグをリストと区別するために、実際のコードでバッグを見てみましょう。

バッグ:

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

リスト

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

3. MultipleBagFetchExceptionの原因

エンティティで2つ以上のバッグを同時にフェッチすると、デカルト積が形成される可能性があります。 Bagには順序がないため、Hibernateは適切な列を適切なエンティティにマップできません。 したがって、この場合、MultipleBagFetchExceptionがスローされます。

MultipleBagFetchException。につながる具体的な例をいくつか見てみましょう。

最初の例では、2つのバッグがあり、両方がイーガーフェッチタイプの単純なエンティティを作成してみましょう。 Artistが良い例かもしれません。 の曲のオファーのコレクションを持つことができます。

それでは、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
}

テストを実行しようとすると、すぐにMultipleBagFetchExceptionが発生し、Hibernate SessionFactoryをビルドできなくなります。そうは言っても、これは行わないでください。

代わりに、コレクションのフェッチタイプの一方または両方をレイジーに変換しましょう。

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

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

これで、テストを作成して実行できるようになります。 ただし、これらのバッグコレクションの両方を同時にフェッチしようとすると、MultipleBagFetchExceptionが発生します。

4. MultipleBagFetchExceptionをシミュレートします

前のセクションでは、 MultipleBagFetchException。 ここでは、統合テストを作成して、これらの主張を検証しましょう。

簡単にするために、 アーティスト 以前に作成したエンティティ。

それでは、統合テストを作成して、両方を取得してみましょう。  と オファー 同時に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));
}

アサーションから、MultipleBagFetchExceptionの根本原因を持つIllegalArgumentExceptionが発生しました。

5. ドメインモデル

考えられる解決策に進む前に、必要なドメインモデルを見てみましょう。これは、後で参照として使用します。

音楽アプリのドメインを扱っているとします。 それを踏まえて、特定のエンティティに焦点を絞りましょう。 アルバム、アーティスト、ユーザー。 

Artist エンティティはすでに見たので、代わりに他の2つのエンティティに進みましょう。

まず、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

}

アルバムにはのコレクションがあり、同時にフォロワーのセットを持つことができます。

次に、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
}

ユーザーは多くのプレイリストを作成できます。 さらに、ユーザーには、 favoriteSongs 用の個別のリストがあり、その順序は配置インデックスに基づいています。

6. 回避策:単一のJPQLクエリでSetを使用する

何よりもまず、それを強調しましょう このアプローチではデカルト積が生成されるため、これは単なる回避策になります。 これは、1つのJPQLクエリで2つのコレクションを同時にフェッチするためです。 対照的に、 設定。 コレクションに順序や重複する要素が必要ない場合は、これが適切な選択です。

このアプローチを示すために、ドメインモデルからAlbumエンティティを参照してみましょう。

アン アルバム エンティティには、songsfollowersの2つのコレクションがあります。 のコレクション  タイプバッグです。 ただし、の場合フォロワー、使用している 設定。 そうは言っても、私たちはに遭遇することはありませんMultipleBagFetchException両方のコレクションを同時にフェッチしようとしても。

統合テストを使用して、単一のJPQLクエリで両方のコレクションをフェッチしながら、IDでAlbumを取得してみましょう。

@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());
}

ご覧のとおり、 アルバムの取得に成功しました。 曲のリストだけがバッグだからです 。 一方、フォロワーのコレクションはSetです。

ちなみに、私たちが利用していることを強調する価値があります QueryHints.HINT_PASS_DISTINCT_THROUGH。 エンティティJPQLクエリを使用しているため、 明確 キーワードが実際のSQLクエリに含まれないようにします。 したがって、残りのアプローチにもこのクエリヒントを使用します。 

7. 回避策:単一のJPQLクエリでリストを使用する

前のセクションと同様に、 これによりデカルト積も生成され、パフォーマンスの問題が発生する可能性があります。 繰り返しますが、 リスト、セット、 またはデータ型のバッグ。 このセクションの目的は、タイプBagが1つしかない場合に、Hibernateがコレクションを同時にフェッチできることをさらに示すことです。

このアプローチでは、 ユーザー ドメインモデルのエンティティ。

前述のように、 ユーザー プレイリストfavoriteSongsの2つのコレクションがあります。 プレイリストには順序が定義されていないため、バッグコレクションになっています。 ただし、favoriteSongsListの場合、その順序はユーザーがそれをどのように配置するかによって異なります FavoriteSong エンティティをよく見ると、ArrangementIndexプロパティによってそれが可能になりました。

繰り返しになりますが、単一のJPQLクエリを使用して、の両方のコレクションをフェッチしながら、すべてのユーザーを取得できるかどうかを確認してみましょう。 プレイリスト と FavoriteSongs 同時に。

実例を示すために、統合テストを作成しましょう。

@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());
}

アサーションから、すべてのユーザーを正常に取得したことがわかります。 さらに、 私たちは遭遇しませんでした MultipleBagFetchException。 これは、2つのコレクションを同時にフェッチしているにもかかわらず、 プレイリスト バッグコレクションです。

8. 理想的な解決策:複数のクエリを使用する

これまでの回避策から、コレクションを同時に取得するために単一のJPQLクエリを使用することがわかりました。 残念ながら、デカルト積が生成されます。 私たちはそれが理想的ではないことを知っています。 だからここで、解決しましょう MultipleBagFetchException パフォーマンスを犠牲にすることなく。

複数のバッグコレクションを持つエンティティを扱っているとします。 私たちの場合、それはですArtistエンティティ。 の曲のオファーの2つのバッグコレクションがあります。

この状況を考えると、 1つのJPQLクエリを使用して両方のコレクションを同時にフェッチすることもできません。 これを行うと、MultipleBagFetchExceptionが発生します。 代わりに、2つのJPQLクエリに分割しましょう。

このアプローチでは、両方のバッグコレクションを一度に1つずつ正常にフェッチすることが期待されます。

もう一度、最後に、すべてのアーティストを取得するための統合テストをすばやく作成しましょう。

@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());
}

テストから、のコレクションを取得しながら、最初にすべてのアーティストを取得しました。

次に、アーティストのオファーを取得するための別のクエリを作成しました。

このアプローチを使用して、MultipleBagFetchExceptionとデカルト積の形成を回避しました。

9. 結論

この記事では、 MultipleBagFetchException 詳細に。 必要な語彙とこの例外の原因について話し合いました。 次に、それをシミュレートしました。 その後、回避策と理想的なソリューションごとに異なるシナリオを持つ単純な音楽アプリのドメインについて話しました。 最後に、それぞれのアプローチを検証するために、いくつかの統合テストを設定しました。

いつものように、記事の完全なソースコードは、GitHubから入手できます。