1. 引言

本文将深入探讨Hibernate中Session接口的几个核心方法:savepersistupdatemergesaveOrUpdaterefreshreplicate之间的区别。这不是Hibernate入门教程,读者应已掌握基本配置、对象关系映射和实体操作知识。若需Hibernate基础,可参考Spring整合Hibernate教程

2. Session作为持久化上下文的实现

Session接口提供了多个最终会保存数据到数据库的方法:persistsaveupdatemergesaveOrUpdate。要理解这些方法的差异,首先必须明确Session作为持久化上下文的作用,以及实体实例相对于Session的不同状态。同时,我们也需要了解Hibernate发展历程中导致部分API方法重复的历史背景。

2.1 管理实体实例

除对象关系映射外,Hibernate解决的另一个核心问题是运行时实体管理。持久化上下文(Persistence Context)是Hibernate对此问题的解决方案。我们可以将其视为在会话期间加载或保存的所有对象的容器或一级缓存。

Session是一个逻辑事务,其边界由业务逻辑定义。当通过持久化上下文操作数据库,且所有实体实例都附加到该上下文时,在会话期间交互的每个数据库记录都应保证有唯一的实体实例。

在Hibernate中,持久化上下文由org.hibernate.Session实例表示;在JPA中则是jakarta.persistence.EntityManager。当使用Hibernate作为JPA提供者并通过EntityManager操作时,其实现本质上封装了底层的Session对象。但Hibernate Session提供了更丰富的接口,有时直接操作Session会更有用。

2.2 实体实例的状态

任何实体实例相对于Session持久化上下文都处于以下三种主要状态之一:

  • 瞬时态(transient):从未附加到任何Session的实例。数据库中没有对应记录,通常是我们为保存到数据库而创建的新对象。
  • 持久态(persistent):与唯一Session对象关联的实例。当Session刷新到数据库时,该实体保证在数据库中有对应的持久化记录。
  • 游离态(detached):曾附加到Session(处于持久态)但现在已分离的实例。当实体被逐出上下文、清除/关闭会话,或经过序列化/反序列化过程时进入此状态。

以下是简化状态图,标注了触发状态转换的Session方法:

Session方法状态转换图

当实体处于持久态时,对映射字段的所有修改将在Session刷新时应用到数据库记录。持久态实例是"在线"的,而游离态实例是"离线"的,不会被Hibernate监控变更。

这意味着修改持久态对象的字段时,无需调用saveupdate等方法保存变更到数据库。我们只需提交事务、刷新会话或关闭会话即可。

2.3 符合JPA规范

Hibernate曾是最成功的Java ORM实现,其API深刻影响了Java持久化API(JPA)规范。但两者仍存在许多差异,包括一些重大和细微的不同。

为实现JPA标准,Hibernate API进行了修订。为匹配EntityManager接口,Session接口新增了多个方法。这些方法与原始方法目的相同,但遵循规范要求,因此存在差异。

3. 操作方法的差异

必须明确:所有方法(persistsaveupdatemergesaveOrUpdate)不会立即生成对应的SQL UPDATEINSERT语句。实际保存到数据库发生在提交事务或刷新Session时。

这些方法本质上通过生命周期状态转换来管理实体实例状态。以下使用注解映射的简单Person实体作为示例:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    // ... getters and setters
}

3.1 persist

persist方法用于将新实体实例添加到持久化上下文,即实现从瞬时态到持久态的转换。

通常在需要添加数据库记录(持久化实体实例)时调用:

Person person = new Person();
person.setName("John");
session.persist(person);

调用persist后发生了什么?person对象从瞬时态转换为持久态。对象现在位于持久化上下文中,但尚未保存到数据库。INSERT语句的生成将在提交事务、刷新或关闭会话时发生。

注意persist方法返回void。它直接修改传入对象的状态,person变量引用实际持久化的对象。

此方法是后期添加到Session接口的。其主要特点是符合JSR-220规范(EJB持久化)。规范严格定义了其语义:

  • 若实例已是持久态,则此调用对该实例无影响(但仍会级联到配置了cascade=PERSISTcascade=ALL的关联)。
  • 若实例是游离态,调用此方法或提交/刷新会话时将抛出异常。

规范未涉及实例标识符的处理,不要求立即生成ID(无论ID生成策略如何)。调用此方法后ID不一定非空,不应依赖其值。

对已持久化实例调用persist无影响,但尝试持久化游离态实例将导致异常。以下代码会失败:

Person person = new Person();
person.setName("John");
session.persist(person);

session.evict(person); // 变为游离态

session.persist(person); // 抛出PersistenceException!

3.2 save

save方法是Hibernate的"原生"方法,不符合JPA规范。

其目的与persist基本相同,但实现细节不同。文档明确说明它会持久化实例,"首先分配生成的标识符",并返回该标识符的Serializable值:

Person person = new Person();
person.setName("John");
Long id = (Long) session.save(person);

对已持久化实例调用save的效果与persist相同。差异在于对游离态实例的处理:

Person person = new Person();
person.setName("John");
Long id1 = (Long) session.save(person);

session.evict(person); // 变为游离态
Long id2 = (Long) session.save(person); // 创建新记录

id2将与id1不同。对游离态实例调用save会创建新的持久化实例并分配新ID,导致提交或刷新时数据库中出现重复记录。

3.3 merge

merge方法的主要目的是用游离态实体实例的新字段值更新持久态实体实例。

例如,假设有一个RESTful接口:一个方法根据ID返回JSON序列化对象,另一个方法接收调用方更新的对象版本。经过序列化/反序列化的实体将处于游离态。

反序列化后,我们需要从持久化上下文获取持久态实体实例,并用游离态实例的新值更新其字段。merge方法正是为此设计:

  • 根据传入对象的ID查找实体实例(从持久化上下文获取或从数据库加载)
  • 将传入对象的字段复制到此实例
  • 返回更新后的实例

以下示例中,我们将保存的实体逐出(变为游离态),修改name字段,然后合并游离态实体:

Person person = new Person(); 
person.setName("John"); 
session.save(person);

session.evict(person); // 变为游离态
person.setName("Mary");

Person mergedPerson = (Person) session.merge(person);

注意merge返回一个对象。它是加载到持久化上下文并更新的mergedPerson对象,而非作为参数传入的person对象。这是两个不同的对象,通常应丢弃person对象。

persist类似,JSR-220规范定义了merge的可靠语义:

  • 若实体是游离态,将其值复制到现有持久态实体
  • 若实体是瞬时态,将其值复制到新创建的持久态实体
  • 对所有配置了cascade=MERGEcascade=ALL的关联执行级联操作
  • 若实体已是持久态,此方法调用对其无效(但仍执行级联)

3.4 update

persistsave类似,update是Hibernate的"原生"方法。其语义在几个关键点有所不同:

  • 直接操作传入对象(返回类型为void),将其从游离态转换为持久态
  • 若传入瞬时态实体,将抛出异常

以下示例中,我们保存对象后将其逐出(变为游离态),修改name并调用update。注意update直接操作person对象本身,无需将结果赋给新变量。本质上是将现有实体实例重新附加到持久化上下文,这是JPA规范不允许的操作:

Person person = new Person();
person.setName("John");
session.save(person);
session.evict(person); // 变为游离态

person.setName("Mary");
session.update(person); // 重新附加

对瞬时态实例调用update将导致异常。以下代码无效:

Person person = new Person();
person.setName("John");
session.update(person); // 抛出PersistenceException!

3.5 saveOrUpdate

此方法仅存在于Hibernate API中,无标准化对应版本。与update类似,也可用于重新附加实例。

实际上,处理update方法的内部类DefaultUpdateEventListenerDefaultSaveOrUpdateListener的子类,仅覆盖了部分功能。saveOrUpdate的主要区别在于应用于瞬时态实例时不会抛出异常,而是将其转换为持久态。以下代码将持久化新创建的Person实例:

Person person = new Person();
person.setName("John");
session.saveOrUpdate(person);

可将其视为使对象持久化的通用工具,无论其处于瞬时态还是游离态。

3.6 refresh

refresh方法同时存在于Hibernate API和JPA中。顾名思义,用于刷新持久态实体,使其与数据库行同步。

调用refresh会覆盖对实体的所有修改。当数据库触发器用于初始化对象属性时特别有用。以下示例刷新新保存的Person实例:

Person person = new Person();
person.setName("John");
session.save(person);
session.flush();
session.refresh(person);

这将重新加载person对象及其所有关联对象和集合,使刷新保存对象后触发的任何修改重新加载。

关键区别refresh用数据库版本刷新持久化上下文中的实体,而merge用持久化上下文中的版本更新数据库中的实体。

3.7 replicate

replicate方法仅存在于Hibernate API中,不属于JPA的EntityManager接口。注意自Hibernate 6.0起此方法已废弃且无替代方案。

有时需要加载持久化实例图并将其复制到另一个数据存储而不重新生成标识符值,这时replicate方法就派上用场:

Session session1 = factory1.openSession();
Person p = session1.get(Person.class, id);
Session session2 = factory2.openSession();
session2.replicate(p, ReplicationMode.LATEST_VERSION);

ReplicationMode决定replicate如何处理与现有数据库行的冲突:

  • ReplicationMode.IGNORE:存在相同ID的数据库行时忽略对象
  • ReplicationMode.OVERWRITE:覆盖相同ID的现有数据库行
  • ReplicationMode.EXCEPTION:存在相同ID的数据库行时抛出异常
  • ReplicationMode.LATEST_VERSION:当行版本号早于对象版本号时覆盖行,否则忽略对象

4. 应该使用哪个方法?

若无特殊需求,应优先使用persistmerge方法,因为它们是标准化的,符合JPA规范。若未来切换持久化提供者,它们也具有可移植性。但有时它们可能不如Hibernate"原生"方法(saveupdatesaveOrUpdate)实用。

5. 废弃方法

从Hibernate 6开始,以下方法被标记为废弃,不再推荐使用:

  • save
  • update
  • saveOrUpdate

6. 结论

本文探讨了Hibernate Session不同方法在运行时管理持久化实体中的作用。我们学习了这些方法如何转换实体实例的生命周期状态,以及为何部分方法存在功能重复。

本文源代码可在GitHub获取