Hibernate 实体生命周期
1. 概述
每个 Hibernate 实体在框架内自然有一个生命周期——它处于瞬态、托管、分离或删除状态。
在概念和技术层面上理解这些状态对于正确使用 Hibernate 至关重要。
要了解处理实体的各种 Hibernate 方法,请查看我们之前的教程 。
2. 辅助方法
在本教程中,我们将始终使用几种辅助方法:
- HibernateLifecycleUtil.getManagedEntities(session) - 我们将使用它从 Session 的内部存储中获取所有托管实体
- DirtyDataInspector.getDirtyEntities() - 我们将使用此方法获取所有标记为“脏”的实体的列表
- HibernateLifecycleUtil.queryCount(query) – 针对嵌入式数据库进行count(*)查询的便捷方法
3. 一切都与持久性上下文有关
在进入实体生命周期的主题之前,首先,我们需要了解 context。
简单地说,context 位于客户端代码和数据存储之间。这是一个暂存区,将持久性数据转换为实体,准备好被客户端代码读取和更改。
从理论上讲,context 是工作单元 模式的一种实现 。它跟踪所有加载的数据,跟踪该数据的更改,并负责最终在业务事务结束时将任何更改同步回数据库。
JPA EntityManager和 Hibernate 的Session是context概念的实现。在整篇文章中,我们将使用 Hibernate Session 来表示 context。
Hibernate 实体生命周期状态解释了实体如何与context相关,我们将在接下来看到。
4. 受管实体
托管实体是数据库表行的表示(尽管该行不必存在于数据库中)。
这是由当前运行的Session管理的,对它所做的每一个更改都会被跟踪并自动传播到数据库。
Session要么从数据库加载实体, 要么重新附加一个分离的实体。我们将在第 5 节中讨论分离的实体。 让我们观察一些代码以获得澄清。
我们的示例应用程序定义了一个实体,即FootballPlayer类。在启动时,我们将使用一些示例数据初始化数据存储:
+-------------------+-------+
| Name | ID |
+-------------------+-------+
| Cristiano Ronaldo | 1 |
| Lionel Messi | 2 |
| Gianluigi Buffon | 3 |
+-------------------+-------+
假设我们想更改布冯的名字,我们想用他的全名 Gianluigi Buffon而不是 Gigi Buffon。
首先,我们需要通过获取一个 Session 来开始我们的工作单元:
Session session = sessionFactory.openSession();
在服务器环境中,我们可以 通过上下文感知代理将Session注入我们的代码。原理还是一样的:我们需要一个Session来封装我们工作单元的业务事务。
接下来,我们将指示我们的 Session从持久存储中加载数据:
assertThat(getManagedEntities(session)).isEmpty();
List<FootballPlayer> players = s.createQuery("from FootballPlayer").getResultList();
assertThat(getManagedEntities(session)).size().isEqualTo(3);
当我们第一次获得 Session时,它的持久上下文存储是空的,如我们的第一个断言语句所示。
接下来,我们正在执行一个查询,该查询从数据库中检索数据,创建数据的实体表示,最后返回实体供我们使用。
在内部, Session跟踪它在持久上下文存储中加载的所有实体。在我们的例子中,Session 的内部存储在查询之后将包含 3 个实体。
现在让我们更改 Gigi 的名字:
Transaction transaction = session.getTransaction();
transaction.begin();
FootballPlayer gigiBuffon = players.stream()
.filter(p -> p.getId() == 3)
.findFirst()
.get();
gigiBuffon.setName("Gianluigi Buffon");
transaction.commit();
assertThat(getDirtyEntities()).size().isEqualTo(1);
assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Gianluigi Buffon");
4.1.它是如何工作的?
在调用事务*commit()*或 *flush()*时,Session将从其跟踪列表中找到任何脏实体并将状态同步到数据库。
请注意,我们不需要调用任何方法来通知 Session我们更改了实体中的某些内容——因为它是一个托管实体,所有更改都会自动传播到数据库。
一个托管实体始终是一个持久实体——它必须有一个数据库标识符,即使尚未创建数据库行表示,即 INSERT 语句正在等待工作单元的结束。
请参阅下面关于瞬态实体的章节。
5. 分离实体
分离实体只是一个普通实体 POJO ,其标识值对应于数据库行。与托管实体的区别在于它不再被任何持久性context跟踪。
当用于加载 实体的Session关闭时,或者当我们调用Session.evict(entity) 或*Session.clear()*时,实体可能会分离。
让我们在代码中看到它:
FootballPlayer cr7 = session.get(FootballPlayer.class, 1L);
assertThat(getManagedEntities(session)).size().isEqualTo(1);
assertThat(getManagedEntities(session).get(0).getId()).isEqualTo(cr7.getId());
session.evict(cr7);
assertThat(getManagedEntities(session)).size().isEqualTo(0);
我们的持久化上下文不会跟踪分离实体的变化:
cr7.setName("CR7");
transaction.commit();
assertThat(getDirtyEntities()).isEmpty();
Session.merge(entity)/Session.update(entity) 可以(重新)附加一个会话:
FootballPlayer messi = session.get(FootballPlayer.class, 2L);
session.evict(messi);
messi.setName("Leo Messi");
transaction.commit();
assertThat(getDirtyEntities()).isEmpty();
transaction = startTransaction(session);
session.update(messi);
transaction.commit();
assertThat(getDirtyEntities()).size().isEqualTo(1);
assertThat(getDirtyEntities().get(0).getName()).isEqualTo("Leo Messi");
有关*Session.merge()和Session.update()*的参考,请参见此处 。
5.1. 身份字段才是最重要的
我们来看看下面的逻辑:
FootballPlayer gigi = new FootballPlayer();
gigi.setId(3);
gigi.setName("Gigi the Legend");
session.update(gigi);
在上面的示例中,我们通过其构造函数以通常的方式实例化了一个实体。我们用值填充了字段,并将标识设置为 3,这对应于属于 Gigi Buffon 的持久数据的标识。调用 update()的效果与我们从另一个持久性context 中加载实体的效果完全相同 。
事实上,Session并不区分重新附加的实体的来源。
从 HTML 表单值构造分离实体是 Web 应用程序中非常常见的场景。
就 Session而言,分离实体只是一个普通实体,其标识值对应于持久数据。
请注意,上面的示例仅用于演示目的。我们需要确切地知道我们在做什么。否则,如果我们只是在要更新的字段上设置值,而其余部分保持不变(因此,实际上是 null),我们最终可能会在我们的实体中得到空值。
6. 瞬态实体
**瞬态实体只是一个*entity *对象,它在持久存储中没有表示,**并且不受任何 Session管理。
瞬态实体的典型示例是通过其构造函数实例化一个新实体。
要使瞬态实体持久化,我们需要调用 *Session.save(entity)*或 Session.saveOrUpdate(entity):
FootballPlayer neymar = new FootballPlayer();
neymar.setName("Neymar");
session.save(neymar);
assertThat(getManagedEntities(session)).size().isEqualTo(1);
assertThat(neymar.getId()).isNotNull();
int count = queryCount("select count(*) from Football_Player where name='Neymar'");
assertThat(count).isEqualTo(0);
transaction.commit();
count = queryCount("select count(*) from Football_Player where name='Neymar'");
assertThat(count).isEqualTo(1);
一旦我们执行 Session.save(entity),实体就会被分配一个标识值并由 Session管理。但是,它可能在数据库中还不可用,因为 INSERT 操作可能会延迟到工作单元结束。
7. 删除实体
**如果*Session.delete(entity)***已被调用,并且 Session已将实体标记为删除,则实体处于已删除(删除)状态。DELETE 命令本身可能在工作单元结束时发出。
让我们在下面的代码中看到它:
session.delete(neymar);
assertThat(getManagedEntities(session).get(0).getStatus()).isEqualTo(Status.DELETED);
但是,请注意,实体会一直保留在持久上下文存储中,直到工作单元结束。