1. 概述
本文将深入探讨 JPA 中实体的管理机制,并分析一个常见问题:当数据库发生外部变更时,持久化上下文可能无法返回最新数据的情况及解决方案。
2. 持久化上下文
每个 EntityManager
都关联着一个持久化上下文(Persistence Context),它在内存中存储所有托管实体。当我们通过 EntityManager
对实体执行任何数据操作时,该实体就会被持久化上下文管理。
当再次查询同一实体时,JPA 会直接从持久化上下文返回托管实体,而不是重新从数据库获取。这种缓存机制避免了重复查询数据库,显著提升了性能。
⚠️ 重要提示:持久化上下文在 JPA 中也被称为一级缓存(L1 缓存)。
3. 场景准备
首先创建一个简单的实体类:
@Entity
@Table(name = "person")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
// 构造方法、getter 和 setter
}
接着创建并持久化一个 Person
实体:
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
Person person = new Person();
person.setName("David Jones");
entityManager.persist(person);
transaction.commit();
4. 不清除托管实体的问题
现在我们通过原生 SQL 更新 Person
的 name
字段。从 EntityManager
获取 JDBC 连接并执行更新:
Session session = entityManager.unwrap(Session.class);
session.doWork(connection -> {
try (PreparedStatement pStmt = connection.prepareStatement("UPDATE person SET name=? WHERE id=?")) {
pStmt.setString(1, "*****");
pStmt.setLong(2, 1);
pStmt.executeUpdate();
}
});
随后通过 EntityManager
再次查询同一实体:
Person updatedPerson = entityManager.find(Person.class, 1);
直观上我们会认为实体名称已被更新为 *****
,但实际验证时却发现:
assertThat(updatedPerson.getName()).isNotEqualTo("*****");
❌ 踩坑警告:名称仍然是 "David Jones"!这是因为 JPA 直接从持久化上下文返回了未更新的托管实体。当数据在
EntityManager
之外被修改时,JPA 无法感知这些变更,也不会更新对应的托管实体。
5. 清除托管实体的解决方案
因此,当我们绕过 EntityManager
直接修改数据库数据时,必须强制清除持久化上下文中的所有托管实体,才能确保后续查询从数据库重新获取数据。
调用 EntityManager
的 clear()
方法即可实现:
entityManager.clear();
此操作会将所有托管实体从持久化上下文中分离,使 JPA 不再跟踪这些实体。
之后再次查询同一实体时(需在 persistence unit 配置中启用 hibernate.show_sql
),控制台会显示:
Hibernate:
select
p1_0.id,
p1_0.name
from
person p1_0
where
p1_0.id=?
这条 SQL 语句表明 EntityManager
确实重新从数据库查询了数据。此时验证实体名称:
assertThat(updatedPerson.getName()).isEqualTo("*****");
✅ 成功解决:实体名称已正确更新为
*****
。
6. 总结
本文深入分析了 JPA 持久化上下文的核心机制。当绕过 EntityManager
直接修改数据库数据时,持久化上下文不会自动重新加载实体。我们需要显式调用 EntityManager.clear()
方法清除所有托管实体,强制后续查询从数据库重新获取数据。
完整示例代码可在 GitHub 仓库 中获取。