1. 引言
本文将深入探讨Hibernate中Session
接口的几个核心方法:save
、persist
、update
、merge
、saveOrUpdate
、refresh
和replicate
之间的区别。这不是Hibernate入门教程,读者应已掌握基本配置、对象关系映射和实体操作知识。若需Hibernate基础,可参考Spring整合Hibernate教程。
2. Session作为持久化上下文的实现
Session
接口提供了多个最终会保存数据到数据库的方法:persist
、save
、update
、merge
和saveOrUpdate
。要理解这些方法的差异,首先必须明确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
刷新时应用到数据库记录。持久态实例是"在线"的,而游离态实例是"离线"的,不会被Hibernate监控变更。
这意味着修改持久态对象的字段时,无需调用save
、update
等方法保存变更到数据库。我们只需提交事务、刷新会话或关闭会话即可。
2.3 符合JPA规范
Hibernate曾是最成功的Java ORM实现,其API深刻影响了Java持久化API(JPA)规范。但两者仍存在许多差异,包括一些重大和细微的不同。
为实现JPA标准,Hibernate API进行了修订。为匹配EntityManager
接口,Session
接口新增了多个方法。这些方法与原始方法目的相同,但遵循规范要求,因此存在差异。
3. 操作方法的差异
必须明确:所有方法(persist
、save
、update
、merge
、saveOrUpdate
)不会立即生成对应的SQL UPDATE
或INSERT
语句。实际保存到数据库发生在提交事务或刷新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=PERSIST
或cascade=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=MERGE
或cascade=ALL
的关联执行级联操作 - 若实体已是持久态,此方法调用对其无效(但仍执行级联)
3.4 update
与persist
和save
类似,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
方法的内部类DefaultUpdateEventListener
是DefaultSaveOrUpdateListener
的子类,仅覆盖了部分功能。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. 应该使用哪个方法?
若无特殊需求,应优先使用persist
和merge
方法,因为它们是标准化的,符合JPA规范。若未来切换持久化提供者,它们也具有可移植性。但有时它们可能不如Hibernate"原生"方法(save
、update
、saveOrUpdate
)实用。
5. 废弃方法
从Hibernate 6开始,以下方法被标记为废弃,不再推荐使用:
save
update
saveOrUpdate
6. 结论
本文探讨了Hibernate Session
不同方法在运行时管理持久化实体中的作用。我们学习了这些方法如何转换实体实例的生命周期状态,以及为何部分方法存在功能重复。
本文源代码可在GitHub获取。