Quick Guide to Hibernate enable_lazy_load_no_trans Property – Hibernate enable_lazy_load_no_trans属性快速指南

最后修改: 2020年 3月 19日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

While using lazy loading in Hibernate, we might face exceptions, saying there is no session.

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

In this tutorial, we’ll discuss how to solve these lazy loading issues. To do this, we’ll use Spring Boot to explore an example.

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

2. Lazy Loading Issues

2.懒惰的加载问题

The aim of lazy loading is to save resources by not loading related objects into memory when we load the main object. Instead, we postpone the initialization of lazy entities until the moment they’re needed. Hibernate uses proxies and collection wrappers to implement lazy loading.

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

When retrieving lazily-loaded data, there are two steps in the process. First, there’s populating the main object, and second, retrieving the data within its proxies. Loading data always requires an open Session in Hibernate.

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

The problem arises when the second step happens after the transaction has closed, which leads to a LazyInitializationException.

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

The recommended approach is to design our application to ensure that data retrieval happens in a single transaction. But, this can sometimes be difficult when using a lazy entity in another part of the code that is unable to determine what has or hasn’t been loaded.

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

Hibernate has a workaround, an enable_lazy_load_no_trans property. Turning this on means that each fetch of a lazy entity will open a temporary session and run inside a separate transaction.

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

3. Lazy Loading Example

3.懒惰加载实例

Let’s look at the behavior of lazy loading under a few scenarios.

让我们看看在几种情况下懒惰加载的行为。

3.1. Set Up Entities and Services

3.1.设置实体和服务

Suppose we have two entities, User and Document. One User may have many Documents, and we’ll use @OneToMany to describe that relationship. Also, we’ll use @Fetch(FetchMode.SUBSELECT) for efficiency.

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

We should note that, by default, @OneToMany has a lazy fetch type.

我们应该注意到,默认情况下,@OneToMany有一个懒惰的获取类型。

Let’s now define our User entity:

现在我们来定义我们的User实体。

@Entity
public class User {

    // other fields are omitted for brevity

    @OneToMany(mappedBy = "userId")
    @Fetch(FetchMode.SUBSELECT)
    private List<Document> docs = new ArrayList<>();
}

Next, we need a service layer with two methods to illustrate the different options. One of them is annotated as @Transactional. Here, both methods perform the same logic by counting all documents from all users:

接下来,我们需要一个有两个方法的服务层来说明不同的选择。其中一个被注释为@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();
    }
}

Now, let’s take a closer look at the following three examples. We’ll also use SQLStatementCountValidator to understand the efficiency of the solution, by counting the number of queries executed.

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

3.2. Lazy Loading With a Surrounding Transaction

3.2.围绕事务的懒惰加载(Lazy Loading with a Surrounding Transaction

First of all, let’s use lazy loading in the recommended way. So, we’ll call our @Transactional method in the service layer:

首先,让我们以推荐的方式使用懒惰加载。因此,我们将在服务层中调用我们的@Transactional方法。

@Test
public void whenCallTransactionalMethodWithPropertyOff_thenTestPass() {
    SQLStatementCountValidator.reset();

    long docsCount = serviceLayer.countAllDocsTransactional();

    assertEquals(EXPECTED_DOCS_COLLECTION_SIZE, docsCount);
    SQLStatementCountValidator.assertSelectCount(2);
}

As we can see, this works and results in two roundtrips to the database. The first roundtrip selects users, and the second selects their documents.

正如我们所看到的,这样做的结果是对数据库的两次往返。第一次往返选择用户,第二次选择他们的文档。

3.3. Lazy Loading Outside of a Transaction

3.3.事务之外的懒惰加载

Now, let’s call a non-transactional method to simulate the error we get without a surrounding transaction:

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

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

As predicted, this results in an error as the getDocs function of User is used outside of a transaction.

正如预测的那样,这导致了一个错误,因为UsergetDocs函数是在一个事务之外使用的。

3.4. Lazy Loading With Automatic Transaction

3.4.自动事务的懒惰加载

To fix that, we can enable the property:

为了解决这个问题,我们可以启用该属性。

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

With the property turned on, we no longer get a LazyInitializationException.

打开该属性后,我们不再得到一个LazyInitializationException

However, the count of the queries shows that six roundtrips have been made to the database. Here, one roundtrip selects users, and five roundtrips select documents for each of five users:

然而,对查询的计数显示,六次往返于数据库。在这里,一次往返选择了用户,五次往返选择了五个用户各自的文件。

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

We’ve run into the notorious N + 1 issue, despite the fact that we set a fetch strategy to avoid it!

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

4. Comparing the Approaches

4.比较各种方法

Let’s briefly discuss the pros and cons.

让我们简单地讨论一下其中的利弊。

With the property turned on, we don’t have to worry about transactions and their boundaries. Hibernate manages that for us.

随着属性的开启,我们不必担心事务和它们的边界。Hibernate为我们管理了这些。

However, the solution works slowly, because Hibernate starts a transaction for us on each fetch.

然而,这个解决方案工作得很慢,因为Hibernate在每次获取时都为我们启动一个事务。

It works perfectly for demos and when we don’t care about performance issues. This may be ok if used to fetch a collection that contains only one element, or a single related object in a one to one relationship.

对于演示和我们不关心性能问题的时候,它可以完美地工作。如果用来获取一个只包含一个元素的集合,或者一个一对一关系的单一相关对象,这可能是可以的。

Without the property, we have fine-grained control of the transactions, and we no longer face performance issues.

如果没有这个属性,我们就可以对事务进行细粒度的控制,而且我们不再面临性能问题。

Overall, this is not a production-ready feature, and the Hibernate documentation warns us:

总的来说,这不是一个可用于生产的功能,而且Hibernate文档也警告我们。

Although enabling this configuration can make LazyInitializationException go away, it’s better to use a fetch plan that guarantees that all properties are properly initialized before the Session is closed.

虽然启用该配置可以使LazyInitializationException消失,但最好使用一个获取计划,保证在会话关闭前正确地初始化所有属性。

5. Conclusion

5.总结

In this tutorial, we explored dealing with lazy loading.

在本教程中,我们探讨了如何处理懒惰加载。

We tried a Hibernate property to help overcome the LazyInitializationException. We also saw how it reduces efficiency and may only be a viable solution for a limited number of use cases.

我们尝试了一个Hibernate属性来帮助克服LazyInitializationException。我们也看到了它是如何降低效率的,并且可能只对有限的用例是一个可行的解决方案。

As always, all code examples are available over on GitHub.

一如既往,所有的代码实例都可以在GitHub上找到