Hibernate抛出的MultipleBagFetchException异常

15

我希望在我的Repository层中有一个选项来急切地加载实体,所以我尝试添加一个方法,应该用所有关系来急切地加载一个问题实体,但它抛出了MultipleBagFetchException。我该如何修复这个问题?我正在使用Hibernate 4.16。

@NamedQuery(name = Question.FIND_BY_ID_EAGER, query = "SELECT q FROM Question q LEFT JOIN FETCH q.answers LEFT JOIN FETCH q.categories LEFT JOIN FETCH q.feedback LEFT JOIN FETCH q.participant WHERE q.id = :id"),
如何使最初使用惰性加载的问题对象变为急切加载,并具有所有关联数据?

如何使最初使用惰性加载的问题对象变为急切加载,并具有所有关联数据?

2个回答

38
这是一个在Hibernate和ORM中普遍存在的相当棘手的问题。
由于多个(fetch)连接,导致创建了相当大的笛卡尔积。即每次连接都会在结果中出现新的列和新的行,导致结果变成了一个相对较大的“方形”。
Hibernate需要从这个表中提炼出一个图形,但它并不足够聪明地将正确的列匹配到正确的实体上。
例如:
假设我们有以下结果。
A B C
A B D

需要变成:

 A
 |
 B
 /\
C  D

Hibernate可以通过主键和一些编码技巧推断出图形必须是什么样子的,但在实践中,它需要明确的帮助才能完成这个任务。

一种方法是在关系上指定Hibernate特定的@IndexColumn或JPA标准的@OrderColumn

例如:

@Entity
public class Question {


    @ManyToMany
    @JoinTable(
        name = "question_to_answer",
        joinColumns = @JoinColumn(name = "question_id"),
        inverseJoinColumns = @JoinColumn(name = "answer_id")
    )
    @IndexColumn(name = "answer_order")
    private List<Answer> answers;

    // ...
}

在这个例子中,我正在使用一个连接表,带有一个额外的列answer_order。通过这个列,每个问题/答案关系都有一个唯一的顺序号,Hibernate可以区分结果表中的条目并创建所需的对象图。
顺便提一下,如果涉及到的实体超过几个,使用如此多的急切连接可能会导致比您根据涉及的实体数量想象的结果集要大得多。
进一步阅读:

2
需要注意的是,@IndexColumn 已被弃用(目前使用的是 Hibernate 5.2.4.Final 版本)。 - JRSofty
2
使用 @OrderColumn 自 JPA 2.0 起。 - qwazer

6
Hibernate不允许获取多个bag,因为这会生成一个笛卡尔积,对于无序列表,在Hibernate术语中称为baggs,即使底层集合没有这些重复行,这也会导致重复条目。因此,当编译JPQL查询时,Hibernate会简单地防止出现这种情况。
现在,你会发现很多答案、博客文章、视频或其他资源告诉你要使用Set而不是List来处理你的集合。 那是可怕的建议。不要这样做! 使用Sets代替Lists将使MultipleBagFetchException消失,但笛卡尔积仍然存在。

正确的解决方法

不要在单个JPQL或Criteria API查询中使用多个JOIN FETCH
List<Post> posts = entityManager
.createQuery(
    "select p " +
    "from Post p " +
    "left join fetch p.comments " +
    "left join fetch p.tags " +
    "where p.id between :minId and :maxId", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.getResultList();

你可以做以下的技巧:
List<Post> posts = entityManager
.createQuery(
    "select distinct p " +
    "from Post p " +
    "left join fetch p.comments " +
    "where p.id between :minId and :maxId ", Post.class)
.setParameter("minId", 1L)
.setParameter("maxId", 50L)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

posts = entityManager
.createQuery(
    "select distinct p " +
    "from Post p " +
    "left join fetch p.tags t " +
    "where p in :posts ", Post.class)
.setParameter("posts", posts)
.setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
.getResultList();

只要使用JOIN FETCH最多获取一个集合,就可以避免笛卡尔积问题。通过使用多个查询,您将避免笛卡尔积,因为除第一个集合以外的任何其他集合都是使用辅助查询获取的。

我尝试了这个解决方案,但是结果的帖子只包含标签(因为它是最后获取的),而不包含评论。评论是惰性加载的。尝试访问posts.get(0).getComments().size()会抛出LazyInitializationException异常。 - Nero
1
所有的代码都在GitHub上,并且运行得非常好。 - Vlad Mihalcea
没错,它奏效了,我必须在我的方法上添加 @Transactional 。在你上面的例子中,如果我使用 Set<Post> 而不是 list,并删除查询提示和 distinct 关键字并像上面一样运行 2 个查询,那么我基本上会得到相同的结果吗? - Nero
你好,我遇到了同样的问题,每个查询都会得到外部查询。我正在使用 Spring,必须创建一个 bean 来获取实体工厂并使用 '@persistenceContext',并将方法标记为 '@transactional',以执行我想要提取的 'bag' 或关系的查询,有没有更简单的方法在 Spring 中实现这一点呢? - qleoz12
感谢@VladMihalcea-,对于Spring Boot项目,是否有一种在实体本身内部完成此操作的方法,或者我们应该寻求创建自定义存储库查询方法的方法? - IcedDante
你需要使用自定义仓库方法模式来完成这个任务。 - Vlad Mihalcea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接