1. 简介
在本篇文章中,我们将详细讲解 Hibernate 中的 MultipleBagFetchException 异常。首先会介绍一些必要的前置知识,然后通过多个示例逐步演示引发该异常的原因,并最终提供一个理想解决方案。
为了更好地说明问题,我们构建了一个简单的音乐应用模型来演示各种解决方式。
2. Hibernate 中的 Bag 是什么?
✅ Bag 与 Java 中的 List
类似,允许包含重复元素,但它不保证顺序。
⚠️ Bag 是 Hibernate 特有的术语,并不属于 Java Collections Framework。
虽然 Bag 和 List
在底层都使用 java.util.List
实现,但在 Hibernate 的语义中它们是不同的:
Bag 的定义:
// @ 任意集合映射注解 private List<T> collection;
List 的定义(注意有
@OrderColumn
):// @ 任意集合映射注解 @OrderColumn(name = "position") private List<T> collection;
📌 总结:没有指定排序字段的 List
就是 Bag。
3. MultipleBagFetchException 出现的原因
当在一个实体中同时 eager 加载两个或以上的 Bag 时,可能会产生笛卡尔积(Cartesian Product)。由于 Bag 没有明确的顺序,Hibernate 无法正确地将列数据映射到对应的实体上,因此抛出 MultipleBagFetchException。
来看一个具体例子:
假设我们要定义一个 Artist
实体,它包含歌曲列表和优惠列表两个 Bag 集合,并且都设置为 eager 加载:
@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。
正确的做法是将其中一个或两个集合改为 lazy 加载:
@OneToMany(mappedBy = "artist")
private List<Song> songs;
@OneToMany(mappedBy = "artist")
private List<Offer> offers;
✅ 这样就可以正常启动程序了。但要注意:如果我们在查询中同时 eager 加载这两个 Bag,仍然会抛出异常。
4. 模拟 MultipleBagFetchException
上一节已经解释了异常的触发条件,下面我们用集成测试来验证一下。
继续使用上面的 Artist
实体,编写如下测试代码:
@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. 域模型结构
在进入解决方案之前,先看看我们的示例模型结构。
我们以一个音乐应用为例,涉及的核心实体包括:
5.1 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
是 Bag。followers
是 Set。
5.2 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
是 Bag。favoriteSongs
是有序 List。
6. 方案一:改用 Set(单条 JPQL 查询)
⚠️ 注意:这种方式会产生笛卡尔积,仅作为临时绕过方案。
✅ 如果其中一个集合是 Set,就不会触发 MultipleBagFetchException。
例如 Album
实体中的 followers
是 Set,所以可以这样写:
@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());
}
📌 只要不是两个 Bag 同时被 fetch,就不会报错。
💡 使用 QueryHints.HINT_PASS_DISTINCT_THROUGH
可避免 JPQL 中的 DISTINCT
影响底层 SQL 执行效率。
7. 方案二:改用 List(单条 JPQL 查询)
⚠️ 同样可能产生笛卡尔积,谨慎使用。
✅ 如果只有一个 Bag,另一个是有序 List,也可以安全加载。
比如 User
实体:
@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());
}
📌 playlists
是 Bag,而 favoriteSongs
是有序 List,因此不会触发异常。
8. 最佳实践:使用多条查询(推荐)
✅ 为了彻底避免笛卡尔积和性能问题,最佳方式是分多次查询。
回到最开始的 Artist
实体——它有两个 Bag:songs
和 offers
。
我们可以分两步来加载:
@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());
}
✅ 两次查询分别加载不同的 Bag,避免了异常和笛卡尔积问题。
9. 总结
本文围绕 Hibernate 中常见的 MultipleBagFetchException
展开了深入分析:
- ✅ 明确了 Bag 与 List 的区别
- ✅ 分析了异常触发的根本原因
- ✅ 提供了三种应对策略:
- ❌ 不推荐:使用 Set 或 List 单次查询(有性能隐患)
- ✅ 推荐:使用多次查询分别加载 Bag 集合
最后提醒一句:虽然 Hibernate 很强大,但在处理复杂关联时,还是要小心设计实体关系,避免不必要的 eager 加载。
📘 完整源码可参考:GitHub 示例项目