Hibernate中的MultipleBagFetchException指南
- Hibernate
- java
1.概述
在本教程中,我們將討論*MultipleBagFetchException 。*我們將從了解必要的術語開始,然後探索一些解決方法,直到找到理想的解決方案。
我們將創建一個簡單的音樂應用程序域,以演示每種解決方案。
2.什麼是Hibernate包?
與List相似,Bag是一個可以包含重複元素的集合。但是,這是不正常的。而且,Bag是Hibernate術語,不是Java Collections Framework的一部分。
給定較早的定義,值得強調的是List和Bag都使用java.util.List 。儘管在Hibernate中,兩者的處理方式有所不同。為了區分Bag和List ,讓我們在實際代碼中對其進行查看。
一個包:
// @ any collection mapping annotation
private List<T> collection;
List
:
// @ any collection mapping annotation
@OrderColumn(name = "position")
private List<T> collection;
3. MultipleBagFetchException
同時在一個實體上提取兩個或多個Bags可以形成笛卡爾積。由於Bag沒有順序,因此Hibernate無法將正確的列映射到正確的實體。因此,在這種情況下,它將引發MultipleBagFetchException 。
我們來看一些導致MultipleBagFetchException的具體示例。
對於第一個示例,讓我們嘗試創建一個簡單的實體,其中包含2個bag,並且兩個bag都帶有EAGER獲取類型。藝術家可能是一個很好的例子。它可以包含歌曲和優惠的集合。
鑑於此,讓我們創建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;
現在,我們將能夠創建和運行測試。雖然,如果我們嘗試同時獲取這兩個bag集合,則仍然會導致MultipleBagFetchException 。
4.模擬MultipleBagFetchException
在上一節中,我們已經看到了MultipleBagFetchException的原因。在這裡,讓我們通過創建集成測試來驗證這些聲明。
為簡單起見,讓我們使用之前創建Artist
現在,讓我們創建集成測試,讓我們嘗試使用JPQL同時songs
和offers
@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));
}
從斷言中,我們遇到了一個IllegalArgumentException ,其根本原因是MultipleBagFetchException 。
5.領域模型
在尋求可能的解決方案之前,讓我們看一下必要的領域模型,稍後將用作參考。
假設我們正在處理音樂應用程序的域。鑑於此,讓我們將注意力集中在某些實體上:專輯,藝術家和用戶。
我們已經看到了Artist實體,因此讓我們繼續其他兩個實體。
首先,讓我們看一下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
}
專輯有一組songs
,同時也可以有一組followers
。
接下來,這是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
}
用戶可以創建許多playlists
。此外,用戶具有一個單獨的“ favoriteSongs
List
,其中其順序基於排列索引。
6.解決方法:在單個JPQL查詢中Set
首先,讓我們強調一下,這種方法將生成笛卡爾乘積,這僅是一種解決方法。 這是因為我們將在單個JPQL查詢中同時獲取兩個集合。相反,使用Set
並沒有錯。如果我們不需要我們的集合有訂單或任何重複的元素,則這是適當的選擇。
為了演示這種方法,讓我們從域模型中引用Album實體。
一個Album
實體有兩個集合: songs
和followers
。 songs
的收集是袋子的類型。但是,對於**followers,
我們使用的是****Set.**
話雖如此,即使我們嘗試同時獲取兩個集合,**也不會遇到**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());
}
如我們所見,我們已經成功檢索了Album
。這是因為只有songs
列表是Bag 。另一方面, followers
的集合是Set
。
附帶說明一下,值得強調的是,我們正在使用QueryHints.HINT_PASS_DISTINCT_THROUGH.
由於我們使用的是實體JPQL查詢,因此可以防止DISTINCT
關鍵字包含在實際的SQL查詢中。因此,我們還將在其餘方法中使用此查詢提示。
7.解決方法:在單個JPQL查詢中List
與上一節類似,這還會生成笛卡爾積,這可能會導致性能問題。同樣,將List, Set,
或Bag用作數據類型也沒有錯。本節的目的是進一步說明,如果Bag類型不多,則Hibernate可以同時獲取集合。
對於這種方法,讓我們使用域模型中User
如前所述, User
有兩個集合: playlists
和favoriteSongs
。該**playlists
都沒有定義的順序,使其成為一個袋子收集。但是,對於favoriteSongs
List
** ,其順序取決於User
排列方式。如果我們仔細查看FavoriteSong
實體,則arrangementIndex
屬性使之成為可能。
同樣,使用單個JPQL查詢,讓我們嘗試驗證是否能夠在同時獲取playlists
和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
。這是因為即使我們同時獲取兩個集合,但只有playlists
才是包集合。
8.理想的解決方案:使用多個查詢
從前面的變通方法中,我們已經看到了使用單個JPQL查詢來同時檢索集合。不幸的是,它會生成笛卡爾積。我們知道這並不理想。所以在這裡,讓我們解決MultipleBagFetchException
而不用犧牲性能。
假設我們正在處理一個實體,該實體具有多個bag collection。在我們的情況下,它是**Artist
實體。它有兩個手袋收藏: songs
和offers
。**
在這種情況下,我們甚至無法使用單個JPQL查詢同時獲取兩個集合。這樣做將導致MultipleBagFetchException
。相反,讓我們將其分為兩個JPQL查詢。
通過這種方法,我們期望一次成功地獲取兩個手袋集合。
同樣,最後一次,讓我們快速創建一個集成測試以檢索所有藝術家:
@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());
}
從測試中,我們首先獲取所有歌手,同時獲取其songs
。
然後,我們創建了另一個查詢以獲取藝術家的offers
。
使用這種方法,我們避免了MultipleBagFetchException
以及笛卡爾積的形成。
9.結論
在本文中,我們詳細MultipleBagFetchException
我們討論了必要的詞彙表和此異常的原因。然後,我們對其進行了仿真。在那之後,我們討論了一個簡單的音樂應用程序的領域,以針對我們的每個變通方法和理想的解決方案提供不同的方案。最後,我們建立了幾個集成測試來驗證每種方法。