JPA EntityManager:为什么在 merge()上使用 persist()?

EntityManager.merge()可以插入新对象并更新现有对象。

为什么要使用persist() (只能创建新对象)?

答案

MyEntity e = new MyEntity();

// scenario 1
// tran starts
em.persist(e); 
e.setSomeField(someValue); 
// tran ends, and the row for someField is updated in the database

// scenario 2
// tran starts
e = new MyEntity();
em.merge(e);
e.setSomeField(anotherValue); 
// tran ends but the row for someField is not updated in the database
// (you made the changes *after* merging)

// scenario 3
// tran starts
e = new MyEntity();
MyEntity e2 = em.merge(e);
e2.setSomeField(anotherValue); 
// tran ends and the row for someField is updated
// (the changes were made to e2, not e)
{
    AnyEntity newEntity;
    AnyEntity nonAttachedEntity;
    AnyEntity attachedEntity;

    // Create a new entity and persist it        
    newEntity = new AnyEntity();
    em.persist(newEntity);

    // Save 1 to the database at next flush
    newEntity.setValue(1);

    // Create a new entity with the same Id than the persisted one.
    AnyEntity nonAttachedEntity = new AnyEntity();
    nonAttachedEntity.setId(newEntity.getId());

    // Save 2 to the database at next flush instead of 1!!!
    nonAttachedEntity.setValue(2);
    attachedEntity = em.merge(nonAttachedEntity);

    // This condition returns true
    // merge has found the already attached object (newEntity) and returns it.
    if(attachedEntity==newEntity) {
            System.out.print("They are the same object!");
    }

    // Set 3 to value
    attachedEntity.setValue(3);
    // Really, now both are the same object. Prints 3
    System.out.println(newEntity.getValue());

    // Modify the un attached object has no effect to the entity manager
    // nor to the other objects
    nonAttachedEntity.setValue(42);
}
AnyEntity myMerge(AnyEntity entityToSave) {
    AnyEntity attached = em.find(AnyEntity.class, entityToSave.getId());
    if(attached==null) {
            attached = new AnyEntity();
            em.persist(attached);
    }
    BeanUtils.copyProperties(attached, entityToSave);

    return attached;
}

如果使用分配的生成器,则使用 merge 而不是 persist 可能会导致冗余的 SQL 语句 ,从而影响性能。

同样, 为托管实体调用合并也是一个错误,因为托管实体由 Hibernate 自动管理,并且在刷新 Persistence Context时, 脏检查机制 会将它们的状态与数据库记录同步。

要了解所有这些工作原理,您首先应该知道 Hibernate 将开发人员的思维方式从 SQL 语句转移到实体状态转换

一旦由 Hibernate 主动管理实体,所有更改将自动传播到数据库。

Hibernate 监视当前连接的实体。但是要使实体成为受管理实体,它必须处于正确的实体状态。

首先,我们必须定义所有实体状态:

  • 新(瞬态)

    从未与 Hibernate Session (也称为Persistence Context )相关联且未映射到任何数据库表行的新创建的对象被视为处于 New(瞬态)状态。

    为了持久化,我们需要显式调用EntityManager#persist方法或使用传递持久性机制。

  • 持久(托管)

    持久性实体已与数据库表行关联,并由当前运行的持久性上下文进行管理。对此类实体所做的任何更改都将被检测到,并传播到数据库(在会话刷新期间)。使用 Hibernate,我们不再需要执行 INSERT / UPDATE / DELETE 语句。 Hibernate 采用事务性的后写式工作方式,并且在当前Session刷新时间的最后一个负责时刻,同步更改。

  • 独立式

    一旦当前正在运行的持久性上下文关闭,所有先前管理的实体都将分离。不再跟踪连续的更改,并且不会发生自动数据库同步。

    要将分离的实体与活动的 Hibernate 会话相关联,可以选择以下选项之一:

    • 重新连接

      Hibernate(但不支持 JPA 2.1)支持通过 Session#update 方法进行重新连接。休眠会话只能将一个实体对象与给定的数据库行关联。这是因为持久性上下文充当内存中的缓存(一级缓存),并且只有一个值(实体)与给定的键(实体类型和数据库标识符)相关联。仅当没有其他 JVM 对象(与同一数据库行匹配)已与当前 Hibernate 会话关联时,才可以重新附加实体。

    • 合并中

    合并将把分离的实体状态(源)复制到托管实体实例(目标)。如果合并实体在当前会话中没有等效项,则将从数据库中获取一个实体。即使在合并操作之后,分离的对象实例也将继续保持分离状态。

  • 已移除

    尽管 JPA 要求只允许删除托管实体,但是 Hibernate 也可以删除分离的实体(但只能通过 Session#delete 方法调用)。仅计划将删除的实体删除,并且实际的数据库 DELETE 语句将在会话刷新期间执行。

为了更好地了解 JPA 状态转换,可以将下图可视化:

在此处输入图片说明

或者,如果您使用特定于 Hibernate 的 API:

在此处输入图片说明

我注意到,当我使用em.merge时,即使没有 JPA 为我生成的字段,我也为每个INSERT都有一条SELECT语句 - 主键字段是我自己设置的 UUID。我切换到em.persist(myEntityObject)然后得到了INSERT语句。

JPA 规范说明了有关persist()的以下内容。

如果X是一个分离的目的, EntityExistsException当 persist 操作被调用时,或者可能抛出EntityExistsException或另一个PersistenceException可以在冲洗被抛出或提交时间。

因此,当对象不应该是分离的对象时,使用persist()将是合适的。您可能更喜欢让代码抛出PersistenceException以使其快速失败。

尽管尚不清楚规范 ,但persist()可能会为对象设置@GeneratedValue @Id 。但是merge()必须具有已生成@Id的对象。

有关合并的一些更多详细信息将帮助您在持久性上使用合并:

返回原始实体以外的托管实例是合并过程的关键部分。如果持久性上下文中已经存在具有相同标识符的实体实例,则提供程序将使用正在合并的实体的状态覆盖其状态,但是必须将已经存在的托管版本返回给客户端,以便可以用过的。如果提供程序未在持久性上下文中更新 Employee 实例,则对该实例的任何引用将与合并的新状态不一致。

在新实体上调用 merge()时,其行为类似于 persist()操作。它将实体添加到持久性上下文中,但是没有添加原始实体实例,而是创建了一个新副本并管理该实例。由 merge()操作创建的副本被持久保存,就像在其上调用了 persist()方法一样。

在存在关系的情况下,merge()操作将尝试更新托管实体,以指向由分离实体引用的实体的托管版本。如果实体与没有持久身份的对象有关系,则合并操作的结果不确定。一些提供程序可能允许托管副本指向非持久对象,而其他提供程序可能会立即引发异常。在这些情况下,可以选择对 merge()操作进行级联,以防止发生异常。我们将在本节后面介绍级联 merge()操作。如果要合并的实体指向已删除的实体,则将抛出 IllegalArgumentException 异常。

延迟加载关系是合并操作中的一种特殊情况。如果在实体分离之前未在实体上触发延迟加载关系,则在合并实体时将忽略该关系。如果关系是在托管时触发的,然后在实体分离时设置为 null,则实体的托管版本将同样在合并过程中清除关系。”

以上所有信息均摘自 Mike Keith 和 Merrick Schnicariol 的 “Pro JPA 2 Mastering the Java™Persistence API”。第 6 章分离和合并部分。这本书实际上是作者专门针对 JPA 的第二本书。这本新书比以前有许多新信息。我真的建议为那些认真参与 JPA 的人阅读本书。对不起,我无意间发布了我的第一个答案。

//MERGE: passedEntity remains unmanaged, but newEntity will be managed
Entity newEntity = em.merge(passedEntity);

//PERSIST: passedEntity will be managed after this
em.persist(passedEntity);
// scenario 2 MY WAY
// tran starts
e = new MyEntity();
e = em.merge(e); // re-assign to the same entity "e"

//access e from jsp and it will work dandy!!

通过回答,有一些有关 “级联” 和 ID 生成的细节缺少。 见问题

另外,值得一提的是,您可以使用单独的Cascade批注进行合并和持久化: Cascade.MERGECascade.PERSIST ,将根据使用的方法进行处理。

该规范是您的朋友;)

我从启发性的 Hibernate 文档中找到了这种解释,因为它们包含一个用例:

merge()的用法和语义对于新用户而言似乎令人困惑。首先,只要您不尝试使用在另一个新实体管理器中的一个实体管理器中加载的对象状态,就根本不需要使用 merge() 。某些整个应用程序永远不会使用此方法。

通常在以下情况下使用 merge():

  • 应用程序在第一个实体管理器中加载对象
  • 对象被传递到表示层
  • 对对象进行了一些修改
  • 该对象向下传递回业务逻辑层
  • 应用程序通过在第二个实体管理器中调用 merge()来保留这些修改

这是 merge()的确切语义:

  • 如果存在当前与持久性上下文关联的具有相同标识符的托管实例,则将给定对象的状态复制到托管实例上
  • 如果当前没有与持久性上下文关联的托管实例,请尝试从数据库中加载它,或创建一个新的托管实例
  • 返回托管实例
  • 给定实例不与持久性上下文相关联,它保持分离状态,通常被丢弃

来自: http : //docs.jboss.org/hibernate/entitymanager/3.6/reference/en/html/objectstate.html