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
我们讨论了必要的词汇表和此异常的原因。然后,我们对其进行了仿真。在那之后,我们讨论了一个简单的音乐应用程序的领域,以针对我们的每个变通方法和理想的解决方案提供不同的方案。最后,我们建立了几个集成测试来验证每种方法。
0 评论