1. 概述
JPA 规范提供了两种数据加载策略:立即加载(eager)和懒加载(lazy)。虽然懒加载能有效避免加载不必要的数据,但当我们需要在已关闭的 Persistence Context 中访问未初始化的数据时,就会遇到问题。尤其是访问懒加载的元素集合(Element Collection),是开发中常见的“踩坑”点。
本文将聚焦于如何正确加载懒加载的元素集合数据,并介绍三种实用解决方案:
- 使用 JPA 查询语言(JPQL)
- 借助实体图(Entity Graph)
- 利用事务传播机制保持 Persistence Context 活跃
这些方案各有适用场景,可根据实际需求灵活选择。
2. Element Collection 的懒加载问题
✅ 默认情况下,@ElementCollection
关联关系采用懒加载策略。这意味着一旦 Persistence Context 关闭,再访问该集合就会抛出异常。
我们通过一个员工与电话号码的模型来演示这个问题:
@Entity
public class Employee {
@Id
private int id;
private String name;
@ElementCollection
@CollectionTable(name = "employee_phone", joinColumns = @JoinColumn(name = "employee_id"))
private List<Phone> phones;
// standard constructors, getters, and setters
}
@Embeddable
public class Phone {
private String type;
private String areaCode;
private String number;
// standard constructors, getters, and setters
}
上面的模型表示:一个员工可以拥有多个电话号码,phones
是一个嵌入式对象(@Embeddable
)的集合。
接下来定义一个 Spring 风格的 Repository:
@Repository
public class EmployeeRepository {
@PersistenceContext
private EntityManager em;
public Employee findById(int id) {
return em.find(Employee.class, id);
}
public void save(Employee employee) {
em.persist(employee);
}
public void remove(int id) {
Employee employee = em.find(Employee.class, id);
if (employee != null) {
em.remove(employee);
}
}
}
现在写一个测试来复现问题:
public class ElementCollectionIntegrationTest {
@Autowired
private EmployeeRepository employeeRepository;
@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 = LazyInitializationException.class)
public void whenAccessLazyCollection_thenThrowLazyInitializationException() {
Employee employee = employeeRepository.findById(1);
// ❌ Persistence Context 已关闭,访问 phones 触发异常
assertThat(employee.getPhones().size(), is(2));
}
}
⚠️ 测试会抛出 LazyInitializationException
,因为 findById
方法执行完后事务结束,Persistence Context 被关闭,此时再访问 phones
集合就会失败。
虽然可以通过将 @ElementCollection(fetch = FetchType.EAGER)
改为立即加载来解决,但这并不是最优解——它会导致每次查询都加载电话数据,造成性能浪费。
3. 使用 JPA 查询语言(JPQL)加载数据
✅ JPQL 允许我们精确控制查询投影,从而主动加载懒加载字段。
我们可以在 EmployeeRepository
中新增一个方法,使用 JOIN FETCH
显式加载 phones
集合:
public Employee findByJPQL(int id) {
return em.createQuery(
"SELECT e FROM Employee e JOIN FETCH e.phones WHERE e.id = :id",
Employee.class)
.setParameter("id", id)
.getSingleResult();
}
📌 关键点:
JOIN FETCH
会在一条 SQL 中通过内连接(INNER JOIN)将主实体和集合数据一起查出- 生成的 SQL 类似:
SELECT e.*, p.*
FROM Employee e
INNER JOIN employee_phone p ON e.id = p.employee_id
WHERE e.id = ?
这样返回的 Employee
对象中的 phones
集合已经是初始化状态,即使在事务外也可安全访问。
4. 使用 Entity Graph 加载数据
Entity Graph 是 JPA 提供的另一种声明式方式,用于定义查询时需要加载的属性图谱。
我们可以在 Repository 中使用它来显式指定加载 phones
字段:
public Employee findByEntityGraph(int id) {
// 创建一个属性图,包含 name 和 phones
EntityGraph<Employee> entityGraph = em.createEntityGraph(Employee.class);
entityGraph.addAttributeNodes("name", "phones");
// 设置查询属性,使用 fetchgraph 模式
Map<String, Object> properties = new HashMap<>();
properties.put("javax.persistence.fetchgraph", entityGraph);
return em.find(Employee.class, id, properties);
}
📌 注意:
fetchgraph
表示只加载图中定义的字段,其他字段为 null- 若想保留其他字段的默认加载行为,可使用
javax.persistence.loadgraph
Entity Graph 的优势在于解耦了加载逻辑与查询语句,适合需要复用加载策略的场景。
5. 在事务范围内加载数据
最简单粗暴但非常有效的办法:延长 Persistence Context 的生命周期。
Spring 的 @Transactional
注解可以将事务和 Persistence Context 绑定到当前线程,直到方法执行完毕。
我们修改测试方法,加上事务注解:
@Test
@Transactional
public void whenUseTransaction_thenFetchResult() {
Employee employee = employeeRepository.findById(1);
// ✅ 此时 Persistence Context 仍处于打开状态
assertThat(employee.getPhones().size(), is(2));
}
📌 原理:
@Transactional
创建了一个事务代理- 事务从方法开始到结束持续存在
- 所有在此期间执行的 JPA 操作共享同一个 Persistence Context
- 因此即使在方法内部访问懒加载集合也不会抛异常
⚠️ 注意:这种方式适用于 Service 层或测试场景,但在 Web 层(如 Controller)滥用可能导致长事务,影响性能。
6. 总结
面对懒加载集合在关闭的 Persistence Context 中无法访问的问题,我们有三种主流解决方案:
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
✅ JPQL + JOIN FETCH |
精确控制查询 | SQL 级别优化,性能好 | 查询耦合度高 |
✅ Entity Graph | 多场景复用加载策略 | 声明式、可复用 | 配置稍复杂 |
✅ @Transactional |
测试或短事务场景 | 简单直接 | 易误用导致长事务 |
📌 实际开发建议:
- 优先考虑 JPQL + JOIN FETCH,控制粒度最细
- 多个接口需要相同加载逻辑时,使用 Entity Graph
- 单元测试中可放心使用
@Transactional
,避免 mock 复杂数据
示例代码已托管至 GitHub:https://github.com/baeldung/tutorials/tree/master/persistence-modules/spring-data-jpa-enterprise-2