Contents

Hibernate 实体生命周期

1. 概述

每个 Hibernate 实体在框架内自然有一个生命周期——它处于瞬态、托管、分离或删除状态。

在概念和技术层面上理解这些状态对于正确使用 Hibernate 至关重要。

要了解处理实体的各种 Hibernate 方法,请查看我们之前的教程

2. 辅助方法

在本教程中,我们将始终使用几种辅助方法:

  • HibernateLifecycleUtil.getManagedEntities(session) - 我们将使用它从 Session 的内部存储中获取所有托管实体
  • DirtyDataInspector.getDirtyEntities() - 我们将使用此方法获取所有标记为“脏”的实体的列表
  • HibernateLifecycleUtil.queryCount(query) – 针对嵌入式数据库进行count(*)查询的便捷方法

3. 一切都与持久性上下文有关

在进入实体生命周期的主题之前,首先,我们需要了解 context

简单地说,context 位于客户端代码和数据存储之间。这是一个暂存区,将持久性数据转换为实体,准备好被客户端代码读取和更改。

从理论上讲,context 是工作单元 模式的一种实现 。它跟踪所有加载的数据,跟踪该数据的更改,并负责最终在业务事务结束时将任何更改同步回数据库。

JPA EntityManager和 Hibernate 的Sessioncontext概念的实现。在整篇文章中,我们将使用 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);

但是,请注意,实体会一直保留在持久上下文存储中,直到工作单元结束。