FetchType.LAZY
用于实体关联时会出现休眠 N+1 问题。如果您执行查询以选择 n 个实体,并且如果您尝试调用实体的惰性关联的任何访问方法,Hibernate 将执行 n 个附加查询来加载延迟获取的对象。
例如,我们有以下具有一对多书籍集合的作者实体:
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String fullName;
@OneToMany(fetch = FetchType.LAZY)
private Set<Book> books;
}
让我们尝试加载所有作者,并打印每个作者的姓名及其书籍收藏大小:
entityManager.createQuery("select a from Author a", Author.class)
.getResultList()
.forEach(a -> System.out.printf("%s had written %d books\n",
a.getFullName(), a.getBooks().size()));
Hibernate将生成的第一个查询是选择所有作者:
SELECT author0_.id AS id1_0_,
author0_.fullName AS fullname2_0_
FROM authors author0_;
之后,当我们在书籍集合上调用size()
method时,需要初始化这个关联,所以Hibernate将执行一个额外的查询:
SELECT books0_.author_id AS author_i4_1_0_,
books0_.id AS id1_1_0_,
books0_.id AS id1_1_1_,
books0_.author_id AS author_i4_1_1_,
books0_.title AS title2_1_1_,
books0_.year AS year3_1_1_
FROM books books0_
WHERE books0_.author_id=?;
除了第一个查询之外,当我们打印书籍数量时,将为每个作者调用 n 次此查询。因此,查询总数将等于 N+1。
Hibernate提供了几种方法来消除此问题:
- 第一个解决方案是使用联接获取:
entityManager.createQuery("select a from Author a left join fetch a.books",
Author.class);
SELECT author0_.id AS id1_0_0_,
books1_.id AS id1_1_1_,
author0_.fullName AS fullname2_0_0_,
books1_.author_id AS author_i4_1_1_,
books1_.title AS title2_1_1_,
books1_.year AS year3_1_1_,
books1_.author_id AS author_i4_1_0__,
books1_.id AS id1_1_0__
FROM authors author0_
LEFT OUTER JOIN books books1_ ON author0_.id=books1_.author_id;
此查询工作正常,但它有一个问题:它不允许我们使用分页,因为限制不会应用于作者。如果指定query.setMaxResults(n)
,Hibernate将获取所有现有行并在内存中进行分页,从而显着增加内存消耗。
- 另一种方法是使用
@BatchSize
懒惰关联:
public class Author {
…
@OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
@BatchSize(size = 10)
private Set<Book> books;
}
Hibernate将创建第一个查询来检索所有作者:
SELECT author0_.id AS id1_0_,
author0_.fullName AS fullname2_0_
FROM authors author0_;
在这种情况下,我们可以轻松地对作者进行分页。然后,当我们在书籍集合上调用size()
method时,Hibernate将执行以下查询:
/* load one-to-many Author.books */
SELECT books0_.author_id AS author_i4_1_1_,
books0_.id AS id1_1_1_,
books0_.id AS id1_1_0_,
books0_.author_id AS author_i4_1_0_,
books0_.title AS title2_1_0_,
books0_.year AS year3_1_0_
FROM books books0_
WHERE books0_.author_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ? /*batch size*/);
此查询将称为 N/M 次,其中 N 是作者的数量,M 是指定的批大小。我们将总共调用 N/M+1 查询。
- 第三种方法是使用返回作者标识符列表的子查询
Hibernate通过设置@Fetch(FetchMode.SUBSELECT)
懒惰关联提供了这个机会:
public class Author {
…
@OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
@Fetch(FetchMode.SUBSELECT)
private Set<Book> books;
}
第一个查询将加载所有作者:
SELECT author0_.id AS id1_0_,
author0_.fullName AS fullname2_0_
FROM authors author0_;
第二个查询将使用作者子查询获取书籍:
SELECT books0_.author_id AS author_i4_1_1_,
books0_.id AS id1_1_1_,
books0_.id AS id1_1_0_,
books0_.author_id AS author_i4_1_0_,
books0_.title AS title2_1_0_,
books0_.year AS year3_1_0_
FROM books books0_
WHERE books0_.author_id in
(SELECT author0_.id
FROM authors author0_);
如果仔细观察 IN 条件,您会发现子查询中的代码几乎重复了第一个查询。如果我们必须执行一次非常复杂的查询两次,它可能会降低性能。为了加快这种情况,我们可以通过第一个查询过滤和页面作者检索他们的 ID。然后我们可以将这些标识符直接传递给第二个查询的子查询:
List<Integer> authorIds = em.createQuery("select a.id from Author a", Integer.class)
.setFirstResult(5)
.setMaxResults(10)
.getResultList();
List<Author> resultList = entityManager.createQuery("select a from Author a"
+ " left join fetch a.books"
+ " where a.id in :authorIds", Author.class)
.setParameter("authorIds", authorIds)
.getResultList();