Working with Lazy Element Collections in JPA – 在JPA中使用懒惰的元素集合

最后修改: 2020年 1月 24日

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

1. Overview

1.概述

The JPA specification provides two different fetch strategies: eager and lazy. While the lazy approach helps to avoid unnecessarily loading data that we don’t need, we sometimes need to read data not initially loaded in a closed Persistence Context. Moreover, accessing lazy element collections in a closed Persistence Context is a common problem.

JPA规范提供了两种不同的获取策略:急切和懒惰。虽然懒惰方法有助于避免不必要地加载我们不需要的数据,但我们有时需要读取封闭的持久化上下文中最初没有加载的数据此外,在封闭的持久化上下文中访问懒惰元素集合是一个常见的问题。

In this tutorial, we’ll focus on how to load data from lazy element collections. We’ll explore three different solutions: one involving the JPA query language, another with the use of entity graphs, and the last one with transaction propagation.

在本教程中,我们将重点讨论如何从懒惰的元素集合中加载数据。我们将探讨三种不同的解决方案:一种涉及JPA查询语言,另一种是使用实体图,最后一种是使用事务传播。

2. The Element Collection Problem

2.元素收集问题

By default, JPA uses the lazy fetch strategy in associations of type @ElementCollection. Thus, any access to the collection in a closed Persistence Context will result in an exception.

默认情况下,JPA在@ElementCollection类型的关联中使用懒惰获取策略。因此,在封闭的持久化上下文中对集合的任何访问都将导致异常。

To understand the problem, let’s define a domain model based on the relationship between the employee and its phone list:

为了理解这个问题,让我们根据员工和其电话列表之间的关系来定义一个领域模型:

@Entity
public class Employee {
    @Id
    private int id;
    private String name;
    @ElementCollection
    @CollectionTable(name = "employee_phone", joinColumns = @JoinColumn(name = "employee_id"))
    private List phones;

    // standard constructors, getters, and setters
}

@Embeddable
public class Phone {
    private String type;
    private String areaCode;
    private String number;

    // standard constructors, getters, and setters
}

Our model specifies that an employee can have many phones. The phone list is a collection of embeddable types. Let’s use a Spring Repository with this model:

我们的模型规定,一个员工可以有很多电话。电话列表是一个可嵌入类型的集合。让我们在这个模型中使用一个Spring存储库。

@Repository
public class EmployeeRepository {

    public Employee findById(int id) {
        return em.find(Employee.class, id);
    }

    // additional properties and auxiliary methods
}

Now, let’s reproduce the problem with a simple JUnit test case:

现在,让我们用一个简单的JUnit测试案例来重现这个问题。

public class ElementCollectionIntegrationTest {

    @Before
    public void init() {
        Employee employee = new Employee(1, "Fred");
        employee.setPhones(
          Arrays.asList(new Phone("work", "+55", "99999-9999"), new Phone("home", "+55", "98888-8888")));
        employeeRepository.save(employee);
    }

    @After
    public void clean() {
        employeeRepository.remove(1);
    }

    @Test(expected = org.hibernate.LazyInitializationException.class)
    public void whenAccessLazyCollection_thenThrowLazyInitializationException() {
        Employee employee = employeeRepository.findById(1);
 
        assertThat(employee.getPhones().size(), is(2));
    }
}

This test throws an exception when we try to access the phone list because the Persistence Context is closed.

该测试当我们试图访问电话列表时抛出一个异常,因为持久性上下文已关闭

We can solve this problem by changing the fetch strategy of the @ElementCollection to use the eager approach. However, fetching the data eagerly isn’t necessarily the best solution, since the phone data always will be loaded, whether we need it or not.

我们可以通过改变@ElementCollection的获取策略来解决这个问题,使用急切的方法。然而,急切地获取数据并不一定是最好的解决方案,因为无论我们是否需要,手机数据总是会被加载。

3. Loading Data with JPA Query Language

3.用JPA查询语言加载数据

The JPA query language allows us to customize the projected information. Therefore, we can define a new method in our EmployeeRepository to select the employee and its phones:

JPA查询语言允许我们自定义预测信息。因此,我们可以在我们的EmployeeRepository中定义一个新的方法来选择雇员及其电话。

public Employee findByJPQL(int id) {
    return em.createQuery("SELECT u FROM Employee AS u JOIN FETCH u.phones WHERE u.id=:id", Employee.class)
        .setParameter("id", id).getSingleResult();
}

The above query uses an inner join operation to fetch the phone list for each employee returned.

上述查询使用内部连接操作来获取每个返回的雇员的电话列表

4. Loading Data with Entity Graph

4.用实体图加载数据

Another possible solution is to use the entity graph feature from JPA. The entity graph makes it possible for us to choose which fields will be projected by JPA queries. Let’s define one more method in our repository:

另一个可能的解决方案是使用JPA的实体图功能实体图使我们有可能选择哪些字段将被JPA查询所预测。让我们在我们的存储库中再定义一个方法。

public Employee findByEntityGraph(int id) {
    EntityGraph entityGraph = em.createEntityGraph(Employee.class);
    entityGraph.addAttributeNodes("name", "phones");
    Map<String, Object> properties = new HashMap<>();
    properties.put("javax.persistence.fetchgraph", entityGraph);
    return em.find(Employee.class, id, properties);
}

We can see that our entity graph includes two attributes: name and phones. So, when JPA translates this to SQL, it’ll project the related columns.

我们可以看到,我们的实体图包括两个属性:姓名和电话。因此,当JPA将其翻译成SQL时,它将投射相关的列。

5. Loading Data in a Transactional Scope

5.在事务性范围内加载数据

Finally, we’re going to explore one last solution. So far, we’ve seen that the problem is related to the Persistence Context life cycle.

最后,我们要探讨最后一个解决方案。到目前为止,我们已经看到这个问题与持久化上下文的生命周期有关。

What happens is that our Persistence Context is transaction-scoped and will remain open until the transaction finishes. The transaction life cycle spans from the beginning to the end of the execution of the repository method.

发生的情况是,我们的持久化上下文是transaction-scoped,并且将保持开放,直到事务完成。事务的生命周期从存储库方法的执行开始到结束。

So, let’s create another test case and configure our Persistence Context to bind to a transaction started by our test method. We’ll keep the Persistence Context open until the test ends:

因此,让我们创建另一个测试案例,并配置我们的Persistence Context以绑定到由我们的测试方法启动的事务。我们将保持Persistence Context的开放,直到测试结束。

@Test
@Transactional
public void whenUseTransaction_thenFetchResult() {
    Employee employee = employeeRepository.findById(1);
    assertThat(employee.getPhones().size(), is(2));
}

The @Transactional annotation configures a transactional proxy around the instance of the related test class. Moreover, the transaction is associated with the thread executing it. Considering the default transaction propagation setting, every Persistence Context created from this method joins to this same transaction. Consequently, the transaction persistence context is bound to the transaction scope of the test method.

@Transactional注解围绕相关测试类的实例配置了一个事务代理。此外,该事务与执行它的线程相关。考虑到默认的事务传播设置,从该方法创建的每个持久化上下文都会加入到这个相同的事务中。因此,事务持久化上下文被绑定到测试方法的事务范围。

6. Conclusion

6.结语

In this tutorial, we evaluated three different solutions to address the problem of reading data from lazy associations in a closed Persistence Context.

在本教程中,我们评估了三种不同的解决方案,以解决在封闭的Persistence Context中从懒惰关联中读取数据的问题

First, we used the JPA query language to fetch the element collections. Next, we defined an entity graph to retrieve the necessary data.

首先,我们使用JPA查询语言来获取元素集合。接下来,我们定义了一个实体图来检索必要的数据。

And, in the ultimate solution, we used the Spring Transaction to keep the Persistence Context open and read the data needed.

而且,在最终的解决方案中,我们使用Spring Transaction来保持Persistence Context的开放,并读取需要的数据。

As always, the example code for this tutorial is available over on GitHub.

一如既往,本教程的示例代码可在GitHub上获得超过