简介
在这篇文章中,我将向你展示如何处理Hibernate在使用Spring Data JPA同时获取多个集合时抛出的MultipleBagFetchException。
MultipleBagFetchException
正如我解释的那样,当你试图一次获取多个List 集合时,Hibernate会抛出MultipleBagFetchException 。
通过尝试一次获取多个一对多或多对多的关联,会产生一个笛卡尔产品,而且,即使Hibernate没有抛出MultipleBagFetchException ,我们仍然希望避免在我们的查询结果集中得到一个笛卡尔产品。
领域模型
让我们假设我们有一个Post 父实体,它与PostComment 实体有一个双向的@OneToMany 关联,与Tag实体有一个单向的@ManyToMany 关联。


Post 实体有一个comments 集合和一个tags 集合,像这样。
@Entity(name = "Post")
@Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
@OneToMany(
mappedBy = "post",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();
@ManyToMany(
cascade = {CascadeType.PERSIST, CascadeType.MERGE}
)
@JoinTable(name = "post_tag",
joinColumns = @JoinColumn(name = "post_id"),
inverseJoinColumns = @JoinColumn(name = "tag_id")
)
private List<Tag> tags = new ArrayList<>();
}
我们的目标是获取一些Post 实体以及它们相关的comments 和tags 集合。
使用Spring Data JPA查询注解获取MultipleBagFetchException
人们采取的第一个方法是创建一个@Query 方法,在comments 和tags 集合上使用JOIN FETCH ,就像下面的例子一样。
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("""
select distinct p
from Post p
left join fetch p.comments
left join fetch p.tags
where p.id between :minId and :maxId
""")
List<Post> findAllWithCommentsAndTags(
@Param("minId") long minId,
@Param("maxId") long maxId
);
}
但是,如果你试图这样做,你的Spring应用程序甚至不会启动,在试图从相关的@Query 注释中创建JPATypedQuery 时,抛出以下MultipleBagFetchException 。
java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException:
cannot simultaneously fetch multiple bags: [
com.vladmihalcea.book.hpjp.spring.data.query.multibag.domain.Post.comments,
com.vladmihalcea.book.hpjp.spring.data.query.multibag.domain.Post.tags
]
at org.hibernate.internal.ExceptionConverterImpl
.convert(ExceptionConverterImpl.java:141)
at org.hibernate.internal.ExceptionConverterImpl
.convert(ExceptionConverterImpl.java:181)
at org.hibernate.internal.ExceptionConverterImpl
.convert(ExceptionConverterImpl.java:188)
at org.hibernate.internal.AbstractSharedSessionContract
.createQuery(AbstractSharedSessionContract.java:757)
at org.hibernate.internal.AbstractSharedSessionContract
.createQuery(AbstractSharedSessionContract.java:114)
at org.springframework.data.jpa.repository.query.SimpleJpaQuery
.validateQuery(SimpleJpaQuery.java:90)
at org.springframework.data.jpa.repository.query.SimpleJpaQuery
.<init>(SimpleJpaQuery.java:66)
at org.springframework.data.jpa.repository.query.JpaQueryFactory
.fromMethodWithQueryString(JpaQueryFactory.java:51)
如何使用Spring Data JPA修复MultipleBagFetchException?
因此,虽然我们不能使用一个JPA查询来获取两个集合,但我们肯定可以使用两个查询来获取我们需要的所有数据。
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("""
select distinct p
from Post p
left join fetch p.comments
where p.id between :minId and :maxId
""")
List<Post> findAllWithComments(
@Param("minId") long minId,
@Param("maxId") long maxId
);
@Query("""
select distinct p
from Post p
left join fetch p.tags
where p.id between :minId and :maxId
""")
List<Post> findAllWithTags(
@Param("minId") long minId,
@Param("maxId") long maxId
);
}
findAllWithComments 查询将获取所需的Post 实体及其相关的PostComment 实体,而findAllWithTags 查询将获取Post 实体及其相关的Tag 实体。
执行两个查询将允许我们在查询结果集中避免笛卡尔积,但我们必须聚合结果,以便我们返回一个单一的Post 条目集合,其中包含初始化的comments 和tags 集合。
而这正是Hibernate一级缓存或持久化上下文可以帮助我们实现这一目标的地方。
PostService 定义了一个findAllWithCommentsAndTags方法,其实现方式如下。
@Service
@Transactional(readOnly = true)
public class PostServiceImpl implements PostService {
@Autowired
private PostRepository postRepository;
@Override
public List<Post> findAllWithCommentsAndTags(
long minId, long maxId) {
List<Post> posts = postRepository.findAllWithComments(
minId,
maxId
);
return !posts.isEmpty() ?
postRepository.findAllWithTags(
minId,
maxId
) :
posts;
}
}
由于@Transactional 注解被放置在类的层面上,所有的方法都将继承它。因此,findAllWithCommentsAndTags 服务方法将在事务性上下文中执行,这意味着两个PostRepository 方法的调用将在同一个持久化上下文中发生。
由于这个原因,findAllWithComments 和findAllWithTags 方法基本上将返回两个List 对象,其中包含相同的Post 对象引用,因为你最多只能有一个实体引用由给定的持久化上下文管理。
当findAllWithComments 方法将获取Post 实体并将它们存储在持久化上下文或一级缓存中时,第二个方法,findAllWithTags ,将只是把现有的Post 实体与从DB中获取的引用合并,这些引用现在包含了初始化的tags 集合。
这样,comments 和tags 集合都将在返回List 和Post 实体给服务方法调用者之前被取走。
在我们的集成测试中,我们可以验证这两个集合已经被初始化了。
List<Post> posts = postService.findAllWithCommentsAndTags(
1L,
POST_COUNT
);
for (Post post : posts) {
assertEquals(
POST_COMMENT_COUNT,
post.getComments().size()
);
assertEquals(
TAG_COUNT,
post.getTags().size()
);
}
正如你所看到的,我们可以读取comments 和tags 集合的大小,即使在持久化上下文被关闭之后,因为它们已经被findAllWithCommentsAndTags 服务方法执行的两个实体查询所获取了。