Contents

Hibernate Session 方法简介

1. 简介

在本教程中,我们将讨论Session接口的几种方法之间的区别:savepersistupdatemergesaveOrUpdate

这不是对 Hibernate 的介绍,我们应该已经了解配置、对象关系映射和使用实体实例的基础知识。有关 Hibernate 的介绍性文章,请访问我们的Hibernate 4 with Spring 教程。

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

Session 接口有几种方法可以最终将数据保存到数据库中:persist save update merge saveOrUpdate 。要了解这些方法之间的区别,我们必须首先讨论Session作为持久性上下文的目的,以及与 Session 相关的实体实例的状态之间的区别。

我们还应该了解导致一些部分重复的 API 方法的 Hibernate 的发展历史。

2.1. 管理实体实例

除了对象关系映射本身之外,Hibernate 解决的问题之一是在运行时管理实体的问题。“持久化上下文”的概念是 Hibernate 对这个问题的解决方案。我们可以将持久性上下文视为我们在会话期间加载或保存到数据库的所有对象的容器或一级缓存。

会话是一个逻辑事务,其边界由应用程序的业务逻辑定义。当我们通过持久化上下文使用数据库,并且我们的所有实体实例都附加到该上下文时,对于会话期间与之交互的每条数据库记录,我们应该始终拥有一个实体实例。

在 Hibernate 中,持久性上下文由*org.hibernate.Session 实例表示。对于 JPA,它是javax.persistence.EntityManager 。当我们使用 Hibernate 作为 JPA 提供者,并通过EntityManager接口进行操作时,该接口的实现基本上包装了底层的Session*对象。然而,Hibernate Session提供了更丰富的接口和更多的可能性,所以有时直接使用**Session是很有用的。

2.2. 实体实例的状态

我们应用程序中的任何实体实例都以与Session持久性上下文相关的三种主要状态之一出现:

  • transient——这个实例没有,也从来没有,附加到一个会话。该实例在数据库中没有对应的行;它通常只是我们为保存到数据库而创建的一个新对象。
  • persistent ——此实例与唯一的Session对象相关联。在将Session刷新到数据库时,保证该实体在数据库中具有相应的一致记录。
  • detached ——这个实例曾经被附加到一个Session(处于*persistent *状态),但现在不是了。如果我们将实例从上下文中逐出,清除或关闭 Session,或者将实例置于序列化/反序列化过程中,则实例将进入此状态。

这是一个简化的状态图,其中包含对使状态转换发生的Session方法的注解:

/uploads/hibernate_save_persist_update_merge_saveorupdate/1.png

当实体实例处于persistent 状态时,我们对该实例的映射字段所做的所有更改都将在刷新Session时应用于相应的数据库记录和字段。*persistent 实例是“在线的”,而detached *的实例是“离线的”并且不被监控变化。

这意味着当我们更改持久对象的字段时,我们不必调用saveupdate或任何这些方法来获取对数据库的这些更改。我们需要做的就是提交事务、刷新会话或关闭会话。

2.3. 符合 JPA 规范

Hibernate 是最成功的 Java ORM 实现。因此,Hibernate 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方法旨在将新的实体实例添加到持久化上下文中,即将实例从transient状态转换为persist状态。

我们通常在要向数据库添加记录时调用它(持久化一个实体实例):

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

调用persist方法后会发生什么?Person对象已从transient状态转变为persist状态。该对象现在处于持久性上下文中,但尚未保存到数据库中。只有在提交事务、刷新或关闭会话时才会生成INSERT语句。

请注意,persist方法具有void返回类型。它“就地”对传递的对象进行操作,改变其状态。person变量引用实际的持久对象。

此方法是稍后添加到Session接口的。该方法的主要区别在于它符合 JSR-220 规范(EJB 持久性)。我们在规范中严格定义了这个方法的语义,它基本上说明了一个transient实例变得persist(并且操作级联到它与cascade=PERSISTcascade=ALL的所有关系):

  • 如果一个实例已经是persist的,那么这个调用对这个特定的实例没有影响(但它仍然级联到它与cascade=PERSISTcascade=ALL的关系)。
  • 如果一个实例是分离的,我们会在调用这个方法时,或者在提交或刷新会话时得到一个异常。

请注意,这里没有涉及实例标识符的内容。规范没有说明 id 将立即生成,无论 id 生成策略如何。persist方法的规范允许实现发出语句以在提交或刷新时生成 id。调用此方法后,id 不一定非空,因此我们不应该依赖它。

我们可以在一个已经persist的实例上调用这个方法,但什么也没有发生。但是如果我们试图持久化一个detached的实例,实现会抛出一个异常。在下面的示例中,我们将persist实体,将其从上下文中evict ,使其变为detached,然后再次尝试persist。对*session.persist()*的第二次调用会导致异常,因此以下代码将不起作用:

Person person = new Person();
person.setName("John");
session.persist(person);
session.evict(person);
session.persist(person); // PersistenceException!

3.2. save

save方法是不符合 JPA 规范的“原始”Hibernate 方法。

其目的与persist基本相同,但实现细节不同。此方法的文档严格说明它会保留实例,“首先分配生成的标识符”。该方法将返回此标识符的Serializable值:

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

保存已经持久化的实例的效果与使用persist相同。当我们尝试保存一个detached的实例时,区别就来了:

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

id2变量将不同于id1。对detached实例的保存调用会创建一个新的persist实例并为其分配一个新标识符,这会在提交或刷新时导致数据库中的重复记录。

3.3. merge

merge方法的主要目的是使用detached实体实例中的新字段值更新persist实体实例。

例如,假设我们有一个 RESTful 接口,其中有一个方法通过其 id 向调用者检索 JSON 序列化对象,以及一个从调用者接收该对象的更新版本的方法。通过这种序列化/反序列化的实体将出现在detached状态。

在反序列化这个实体实例之后,我们需要从持久化上下文中获取一个persist实体实例,并用这个detached实例的新值更新它的字段。所以merge方法正是这样做的:

  • 通过从传递的对象获取的 id 查找实体实例(检索持久性上下文中的现有实体实例,或从数据库加载的新实例)
  • 将字段从传递的对象复制到此实例
  • 返回一个新更新的实例

在以下示例中,我们从上下文中evict (分离)保存的实体,更改name 字段,然后合并detached的实体:

Person person = new Person(); 
person.setName("John"); 
session.save(person);
session.evict(person);
person.setName("Mary");
Person mergedPerson = (Person) session.merge(person);

请注意,merge方法返回一个对象。它是我们加载到持久化上下文中并更新的merge Person对象,而不是我们作为参数传递的person对象。它们是两个不同的对象,我们通常需要丢弃person对象。

persist方法一样, JSR-220 指定merge方法具有我们可以依赖的某些语义:

  • 如果实体是detached的,它会复制现有的persist实体。
  • 如果实体是transient的,它会复制一个新创建的persist实体。
  • 此操作级联所有具有cascade=MERGEcascade=ALL映射的关系。
  • 如果实体是persist的,则此方法调用对其没有影响(但级联仍然发生)。

3.4. update

与 persist 和 save一样,update方法是一个“原始”的 Hibernate 方法。它的语义在几个关键点上有所不同:

  • 它作用于传递的对象(其返回类型为void)。update方法将传递的对象从detached状态转换为persist状态。
  • 如果我们将transient实体传递给此方法,则会引发异常。

在以下示例中,我们save对象,将其从上下文中逐出(分离),然后更改其name并调用update。请注意,我们没有将update操作的结果放在单独的变量中,因为update发生在person对象本身上。基本上,我们将现有的实体实例重新附加到持久性上下文,JPA 规范不允许我们这样做:

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

尝试在transient实例上调用update将导致异常。以下将不起作用:

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

3.5. saveOrUpdate

此方法仅出现在 Hibernate API 中,没有标准化的对应方法。与update类似,我们也可以使用它来重新附加实例。

实际上,处理update方法的内部DefaultUpdateEventListener类是DefaultSaveOrUpdateListener的子类,只是覆盖了一些功能。saveOrUpdate方法的主要区别在于它在应用于transient实例时不会抛出异常,而是使这个transient实例persist。以下代码将持久化一个新创建的Person实例:

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

我们可以将此方法视为一种通用工具,用于使对象persist,而不管其状态如何,无论是transient的还是detached的。

4. 使用什么?

如果我们没有任何特殊要求,我们应该坚持使用persistmerge方法,因为它们是标准化的并且符合JPA 规范。

如果我们决定切换到另一个持久性提供程序,它们也是可移植的;但是,它们有时看起来不如“原始” Hibernate 方法saveupdatesaveOrUpdate有用。