解决Hibernate N+1问题的3种方法

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提供了几种方法来消除此问题:

  1. 第一个解决方案是使用联接获取:
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将获取所有现有行并在内存中进行分页,从而显着增加内存消耗。

  1. 另一种方法是使用@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 查询。

  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();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值