Contents

Hibernate 延迟加载

1. 概述

在 Hibernate 中使用延迟加载时,我们可能会遇到异常,说没有会话。

在本教程中,我们将讨论如何解决这些延迟加载问题。为此,我们将使用 Spring Boot 来探索一个示例。

2. 延迟加载问题

延迟加载的目的是通过在加载主对象时不将相关对象加载到内存中来节省资源。相反,我们将惰性实体的初始化推迟到需要它们的那一刻。Hibernate 使用代理和集合包装器来实现延迟加载。

检索延迟加载的数据时,过程中有两个步骤。首先,填充主对象,其次,检索其代理中的数据。加载数据总是需要在 Hibernate 中打开Session

当事务关闭后第二步发生时,问题就出现了,这会导致LazyInitializationException

推荐的方法是设计我们的应用程序以确保数据检索发生在单个事务中。但是,当在无法确定已加载或未加载内容的另一部分代码中使用惰性实体时,这有时会很困难。

Hibernate 有一个解决方法,一个enable_lazy_load_no_trans属性。打开它意味着每次获取惰性实体都将打开一个临时会话并在单独的事务中运行。

3. 懒加载示例

让我们看一下延迟加载在几种情况下的行为。

3.1. 设置实体和服务

假设我们有两个实体,UserDocument。一个User可能有多个Document,我们将使用*@OneToMany来描述这种关系。此外,我们将使用@Fetch(FetchMode.SUBSELECT)*来提高效率。

我们应该注意,默认情况下,@OneToMany具有惰性获取类型。

现在让我们定义我们的User实体:

@Entity
public class User {
    // other fields are omitted for brevity
    @OneToMany(mappedBy = "userId")
    @Fetch(FetchMode.SUBSELECT)
    private List<Document> docs = new ArrayList<>();
}

接下来,我们需要一个具有两种方法的服务层来说明不同的选项。其中之一被注释为*@Transactional*。在这里,两种方法通过计算所有用户的所有文档来执行相同的逻辑:

@Service
public class ServiceLayer {
    @Autowired
    private UserRepository userRepository;
    @Transactional(readOnly = true)
    public long countAllDocsTransactional() {
        return countAllDocs();
    }
    public long countAllDocsNonTransactional() {
        return countAllDocs();
    }
    private long countAllDocs() {
        return userRepository.findAll()
            .stream()
            .map(User::getDocs)
            .mapToLong(Collection::size)
            .sum();
    }
}

现在,让我们仔细看看以下三个示例。我们还将使用SQLStatementCountValidator通过计算执行的查询数来了解解决方案的效率。

3.2. 延迟加载周围事务

首先,让我们按照推荐的方式使用延迟加载。因此,我们将在服务层调用我们的*@Transactional*方法:

@Test
public void whenCallTransactionalMethodWithPropertyOff_thenTestPass() {
    SQLStatementCountValidator.reset();
    long docsCount = serviceLayer.countAllDocsTransactional();
    assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount);
    SQLStatementCountValidator.assertSelectCount(2);
}

正如我们所见,这有效并导致两次往返数据库。第一次往返选择用户,第二次选择他们的文档。

3.3. 事务外的延迟加载

现在,让我们调用一个非事务方法来模拟我们在没有周围事务的情况下得到的错误:

@Test(expected = LazyInitializationException.class)
public void whenCallNonTransactionalMethodWithPropertyOff_thenThrowException() {
    serviceLayer.countAllDocsNonTransactional();
}

正如预测的那样,这会导致错误,因为 User的 getDocs函数是在事务之外使用的。

3.4. 延迟加载自动交易

要解决这个问题,我们可以启用该属性:

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

启用该属性后,我们将不再获得LazyInitializationException

但是,查询计数显示对数据库进行了六次往返。在这里,一次往返选择用户,五次往返为五个用户中的每一个选择文档:

@Test
public void whenCallNonTransactionalMethodWithPropertyOn_thenGetNplusOne() {
    SQLStatementCountValidator.reset();
    
    long docsCount = serviceLayer.countAllDocsNonTransactional();
    
    assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount);
    SQLStatementCountValidator.assertSelectCount(EXPECTED_USERS_COUNT + 1);
}

我们遇到了臭名昭著的 N+1 问题,尽管我们设置了一个获取策略来避免它!

4. 比较方法

让我们简要讨论一下利弊。

**开启了这个属性,我们就不用担心事务和它们的边界了。**Hibernate 为我们管理它。

然而,该解决方案运行缓慢,因为 Hibernate 在每次提取时为我们启动一个事务。

它非常适合演示以及当我们不关心性能问题时。如果用于获取仅包含一个元素的集合或一对一关系中的单个相关对象,这可能没问题。

**没有这个属性,我们就可以对交易进行细粒度的控制,**并且我们不再面临性能问题。

总的来说,这不是一个生产就绪的特性,Hibernate 文档警告我们:

尽管启用此配置可以使LazyInitializationException消失,但最好使用提取计划来保证在 Session 关闭之前正确初始化所有属性。