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

JPA EntityManager: Why use persist() over merge()?

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

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


无论哪种方式都会将实体添加到PersistenceContext中,区别在于您之后对实体执行的操作。

Persist接受实体实例,将其添加到上下文并使该实例得到管理(即将跟踪对该实体的未来更新)。

合并创建实体的新实例,从提供的实体复制状态,并管理新副本。您传入的实例将不会被管理(您所做的任何更改都不会成为交易的一部分 - 除非您再次调用合并)。

也许代码示例会有所帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

场景1和3大致相同,但在某些情况下您需要使用场景2。


持久和合并有两个不同的目的(它们根本不是替代品)。

(编辑扩大差异信息)

坚持:

  • 将新寄存器插入数据库
  • 将对象附加到实体管理器。

合并:

  • 找到具有相同ID的附加对象并更新它。
  • 如果存在则更新并返回已附加的对象。
  • 如果不存在,则将新寄存器插入数据库。

persist()效率:

  • 将新寄存器插入数据库比使用merge()更有效。
  • 它不会复制原始对象。

persist()语义:

  • 它确保您正在插入而不是错误地更新。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
    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);
}

这种方式只为实体管理器中的任何寄存器存在1个附加对象。

对于具有id的实体,merge()类似于:

1
2
3
4
5
6
7
8
9
10
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;
}

虽然如果连接到MySQL,merge()可以像使用INSERT with ON DUPLICATE KEY UPDATE选项调用persist()一样高效,但JPA是一个非常高级的编程,你不能认为这种情况在任何地方都是如此。


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

此外,为托管实体调用合并也是一个错误,因为托管实体由Hibernate自动管理,并且在刷新持久性上下文时,它们的状态通过脏检查机制与数据库记录同步。

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

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

Hibernate监视当前附加的实体。但是对于要管理的实体,它必须处于正确的实体状态。

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

  • 新(瞬态)

    新创建的对象(以前未与Hibernate Session(a.k.a Persistence Context)关联且未映射到任何数据库表行)被视为处于新(暂停)状态。

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

  • 持久性(管理)

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

  • 超脱

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

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

    • 重新连接

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

    • 合并

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

  • 删除

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

要更好地理解JPA状态转换,您可以可视化以下图表:

enter image description here

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

enter image description here


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


JPA规范说明以下关于persist()

If X is a detached object, the EntityExistsException may be thrown when the persist
operation is invoked, or the EntityExistsException or another PersistenceException may be thrown at flush or commit time.

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

虽然规范不清楚,但persist()可能会为对象设置@GeneratedValue @Id。但是merge()必须有一个已生成@Id的对象。


mergepersist之间存在一些差异(我将再次枚举已在此处发布的内容):

D1。 merge不会使传递的实体受到管理,而是返回另一个受管理的实例。另一方面的persist将使传递的实体得到管理:

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

//PERSIST: passedEntity will be managed after this
em.persist(passedEntity);

D2。如果删除实体然后决定将实体保留回来,则只能使用persist(),因为merge将抛出IllegalArgumentException

D3。如果您决定手动处理您的ID(例如使用UUID),那么merge
操作将触发后续的SELECT查询,以便查找具有该ID的现有实体,而persist可能不需要这些查询。

D4。有些情况下,您根本不信任调用代码的代码,并且为了确保没有数据更新,而是插入,您必须使用persist


有关合并的更多详细信息将帮助您使用merge over persist:

Returning a managed instance other than the original entity is a critical part of the merge
process. If an entity instance with the same identifier already exists in the persistence context, the
provider will overwrite its state with the state of the entity that is being merged, but the managed
version that existed already must be returned to the client so that it can be used. If the provider did not
update the Employee instance in the persistence context, any references to that instance will become
inconsistent with the new state being merged in.

When merge() is invoked on a new entity, it behaves similarly to the persist() operation. It adds
the entity to the persistence context, but instead of adding the original entity instance, it creates a new
copy and manages that instance instead. The copy that is created by the merge() operation is persisted
as if the persist() method were invoked on it.

In the presence of relationships, the merge() operation will attempt to update the managed entity
to point to managed versions of the entities referenced by the detached entity. If the entity has a
relationship to an object that has no persistent identity, the outcome of the merge operation is
undefined. Some providers might allow the managed copy to point to the non-persistent object,
whereas others might throw an exception immediately. The merge() operation can be optionally
cascaded in these cases to prevent an exception from occurring. We will cover cascading of the merge()
operation later in this section. If an entity being merged points to a removed entity, an
IllegalArgumentException exception will be thrown.

Lazy-loading relationships are a special case in the merge operation. If a lazy-loading
relationship was not triggered on an entity before it became detached, that relationship will be
ignored when the entity is merged. If the relationship was triggered while managed and then set to null while the entity was detached, the managed version of the entity will likewise have the relationship cleared during the merge."

以上所有信息均来自Mike Keith和Merrick Schnicariol的"Pro JPA 2掌握Java?Persistence API"。第6章部分分离和合并。这本书实际上是作者专门撰写JPA的第二本书。这本新书有许多新信息,然后是前一本。我真的建议您阅读本书,了解那些认真参与JPA的人。我很抱歉无意中发布了我的第一个答案。


我在我的实体上得到了lazyLoading异常,因为我试图访问一个在会话中的延迟加载的集合。

我要做的是在一个单独的请求中,从会话中检索实体,然后尝试访问我的jsp页面中的一个有问题的集合。

为了缓解这种情况,我更新了控制器中的同一个实体并将其传递给我的jsp,虽然我想当我在会话中重新保存时它也可以通过SessionScope访问并且不会抛出LazyLoadingException,这是一个修改例2:

以下对我有用:

1
2
3
4
5
6
// 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!!

JPA is indisputably a great simplification in the domain of enterprise
applications built on the Java platform. As a developer who had to
cope up with the intricacies of the old entity beans in J2EE I see the
inclusion of JPA among the Java EE specifications as a big leap
forward. However, while delving deeper into the JPA details I find
things that are not so easy. In this article I deal with comparison of
the EntityManager’s merge and persist methods whose overlapping
behavior may cause confusion not only to a newbie. Furthermore I
propose a generalization that sees both methods as special cases of a
more general method combine.

坚持实体

与合并方法相比,持久化方法非常简单直观。持久化方法的最常见用法可以总结如下:

"将新创建的实体类实例传递给persist方法。此方法返回后,管理并计划实体插入数据库。可能发生在事务提交之前或之前或调用flush方法时。如果实体通过标有PERSIST级联策略的关系引用另一个实体,则该程序也适用于此。"

enter image description here

规范更详细,但是,记住它们并不重要,因为这些细节仅涵盖或多或少的异国情况。

合并实体

与持久化相比,合并行为的描述并不那么简单。没有主要场景,因为它是持久化的,并且程序员必须记住所有场景才能编写正确的代码。在我看来,JPA设计者希望有一些方法,其主要关注点是处理分离的实体(与主要处理新创建的实体的持久化方法相反。)合并方法的主要任务是从状态转移状态。非托管实体(作为参数传递)到持久化上下文中的托管对应方。然而,该任务进一步分为几种情况,这些情况恶化了整个方法行为的可懂度。

我没有重复JPA规范中的段落,而是准备了一个流程图,该流程图示意性地描述了合并方法的行为:

enter image description here

那么,什么时候应该使用persist和merge?

坚持

  • 您希望该方法始终创建一个新实体,并且永远不会更新实体。否则,该方法会因主键唯一性违规而抛出异常。
  • 批处理过程,以有状态方式处理实体(请参阅网关模式)。
  • 性能优化

合并

  • 您希望该方法在数据库中插入或更新实体。
  • 您希望以无状态方式处理实体(服务中的数据传输对象)
  • 您想要插入一个新实体,该实体可能具有对可能但尚未创建的另一个实体的引用(关系必须标记为MERGE)。例如,插入新照片时引用新的或预先存在的相册。


我从Hibernate docs中发现了这个解释,因为它们包含一个用例:

The usage and semantics of merge() seems to be confusing for new users. Firstly, as long as you are not trying to use object state loaded in one entity manager in another new entity manager, you should not need to use merge() at all. Some whole applications will never use this method.

Usually merge() is used in the following scenario:

  • The application loads an object in the first entity manager
  • the object is passed up to the presentation layer
  • some modifications are made to the object
  • the object is passed back down to the business logic layer
  • the application persists these modifications by calling merge() in a second entity manager

Here is the exact semantic of merge():

  • if there is a managed instance with the same identifier currently associated with the persistence context, copy the state of the given object onto the managed instance
  • if there is no managed instance currently associated with the persistence context, try to load it from the database, or create a new managed instance
  • the managed instance is returned
  • the given instance does not become associated with the persistence context, it remains detached and is usually discarded

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


通过答案,有关"Cascade"和id生成的一些细节缺失。看问题

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

规格是你的朋友;)


场景X:

表:Spitter(一),表:Spittles(很多)(Spittles是与FK的关系的所有者:spitter_id)

这种情况导致节省:Spitter和Spittles都好像拥有Same Spitter一样。

1
2
3
4
5
6
7
8
9
10
11
        Spitter spitter=new Spitter();  
    Spittle spittle3=new Spittle();    
    spitter.setUsername("George");
    spitter.setPassword("test1234");
    spittle3.setSpittle("I love java 2");      
    spittle3.setSpitter(spitter);              
    dao.addSpittle(spittle3); // <--persist    
    Spittle spittle=new Spittle();
    spittle.setSpittle("I love java");
    spittle.setSpitter(spitter);        
    dao.saveSpittle(spittle); //<-- merge!!

情景Y:

这将节省Spitter,将节省2 Spittles但他们不会引用相同的Spitter!

1
2
3
4
5
6
7
8
9
10
11
        Spitter spitter=new Spitter();  
    Spittle spittle3=new Spittle();    
    spitter.setUsername("George");
    spitter.setPassword("test1234");
    spittle3.setSpittle("I love java 2");      
    spittle3.setSpitter(spitter);              
    dao.save(spittle3); // <--merge!!      
    Spittle spittle=new Spittle();
    spittle.setSpittle("I love java");
    spittle.setSpitter(spitter);        
    dao.saveSpittle(spittle); //<-- merge!!


另一个观察:

当您的表中已存在具有此ID的记录时,merge()仅关注自动生成的ID(在IDENTITYSEQUENCE上测试)。在这种情况下,merge()将尝试更新记录。
但是,如果缺少id或者不匹配任何现有记录,merge()将完全忽略它并要求db分配新记录。这有时是很多错误的根源。不要使用merge()强制新记录的ID。

另一方面,persist()永远不会让你甚至传递一个id。它会立即失败。就我而言,它是:

Caused by: org.hibernate.PersistentObjectException: detached entity
passed to persist

hibernate-jpa javadoc有一个提示:

Throws: javax.persistence.EntityExistsException - if the entity
already exists. (If the entity already exists, the
EntityExistsException may be thrown when the persist operation is
invoked, or the EntityExistsException or another PersistenceException
may be thrown at flush or commit time.)


您可能来这里是关于何时使用persist以及何时使用merge的建议。我认为这取决于情况:您需要创建新记录的可能性有多大,以及检索持久数据有多难。

我们假设您可以使用自然键/标识符。

  • 数据需要保留,但偶尔存在记录并且需要更新。在这种情况下,您可以尝试持久化,如果它抛出EntityExistsException,您可以查找它并组合数据:

    试试{entityManager.persist(entity)}

    catch(EntityExistsException异常){/ *检索并合并* /}

  • 持久化数据需要更新,但偶尔也没有数据记录。在这种情况下,您查找它,并在实体丢失时执行持久性:

    entity = entityManager.find(key);

    if(entity == null){entityManager.persist(entity); }

    别的{/ *合并* /}

如果您没有自然键/标识符,那么您将很难确定实体是否存在,或者如何查找实体。

合并也可以通过两种方式处理:

  • 如果更改通常很小,请将它们应用于托管实体。
  • 如果更改很常见,请从持久化实体复制ID,以及未更改的数据。然后调用EntityManager :: merge()来替换旧内容。

  • persist(entity)应该与全新的实体一起使用,将它们添加到DB中(如果实体已经存在于DB中,则会抛出EntityExistsException)。

    应该使用merge(实体),如果实体已分离并已更改,则将实体放回持久性上下文。

    可能持久化是生成INSERT sql语句并合并UPDATE sql语句(但我不确定)。