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

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

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

717254 次浏览

无论哪种方式都会将一个实体添加到持久上下文中,区别在于你之后对实体做什么。

持久化获取一个实体实例,将其添加到上下文中并使该实例受到管理(即将跟踪对实体的未来更新)。

Merge返回与状态合并的托管实例。它确实会返回PeristenceContext中存在的内容或创建您的实体的新实例。无论如何,它将从提供的实体复制状态,并返回托管副本。您传入的实例将不会被管理(您所做的任何更改都不会成为事务的一部分-除非您再次调用合并)。尽管您可以使用返回的实例(托管实例)。

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

MyEntity e = new MyEntity();
// scenario 1// tran startsem.persist(e);e.setSomeField(someValue);// tran ends, and the row for someField is updated in the database
// scenario 2// tran startse = 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 startse = 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。

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

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

为了缓解这个问题,我在控制器中更新了相同的实体并将其传递给我的jsp,尽管我想象当我在会话中重新保存时,它也可以通过SessionScope访问,而不是抛出LazyLoadingException,这是示例2的修改:

以下内容为我工作:

// scenario 2 MY WAY// tran startse = new MyEntity();e = em.merge(e); // re-assign to the same entity "e"
//access e from jsp and it will work dandy!!

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

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

如果X是一个分离的对象,则EntityExistsException可能会在持久化时抛出操作被调用,或者EntityExistsException或另一个PersistenceException可能会在刷新或提交时抛出。

因此,当对象不应该是一个分离的对象时,使用persist()是合适的。您可能更喜欢让代码抛出PersistenceException,这样它就会快速失败。

尽管规格不清楚persist()可能会为对象设置@GeneratedValue@Id。然而,merge()必须有一个已经生成@Id的对象。

有关合并的更多详细信息,这将帮助您使用合并而不是持久化:

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

当在一个新实体上调用合并()时,它的行为类似于坚持()操作。它添加了实体到持久性上下文,但不是添加原始实体实例,而是创建一个新的而是复制并管理该实例。由合并()操作创建的副本将被持久化就像在它上面调用了坚持()方法一样。

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

延迟加载关系是合并操作中的特例。如果延迟加载关系在实体脱离之前没有在实体上触发,该关系将被当实体合并时忽略。如果关系在托管时被触发,然后在实体分离时设置为空,则实体的托管版本同样会在合并期间清除关系。"

以上所有信息都摘自Mike Keith和Merrick Schnicariol的“Pro JPA 2 Mastering theJava™Per的API”。第6章。部分分离和合并。这本书实际上是作者致力于JPA的第二本书。这本新书比前一本有许多新信息。我真的建议那些将认真参与JPA的人阅读这本书。很抱歉匿名发布我的第一个答案。

方案十:

表:吐痰(一个),表:唾沫(许多)(唾沫是与FK关系的所有者:spitter_id)

这种情况导致保存:吐痰和两个唾沫,就好像属于同一个吐痰。

        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); // <--persistSpittle spittle=new Spittle();spittle.setSpittle("I love java");spittle.setSpitter(spitter);dao.saveSpittle(spittle); //<-- merge!!

方案Y:

这将节省吐痰,将节省2唾沫但他们不会引用相同的吐痰!

        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的附加对象并更新它。
  • 如果存在,则更新并返回已附加的对象。
  • 如果不存在,则将新寄存器插入数据库。

持续()效率:

  • 在向数据库插入新寄存器时,它可能比合并()更有效。
  • 它不会重复原始对象。

坚持()语义学:

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

示例:

{AnyEntity newEntity;AnyEntity nonAttachedEntity;AnyEntity attachedEntity;
// Create a new entity and persist itnewEntity = new AnyEntity();em.persist(newEntity);
// Save 1 to the database at next flushnewEntity.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 valueattachedEntity.setValue(3);// Really, now both are the same object. Prints 3System.out.println(newEntity.getValue());
// Modify the un attached object has no effect to the entity manager// nor to the other objectsnonAttachedEntity.setValue(42);}

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

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

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合并()可以像使用带有ON DUPLICATE KEY UPDATE选项的INSERT调用一样高效,但JPA是一个非常高级的编程,您不能假设这种情况到处都是。

mergepersist之间还有一些差异(我将再次列举那些已经在这里发布的):

D1.merge不会使传递的实体被管理,而是返回另一个被管理的实例。另一边的persist将使传递的实体被管理:

//MERGE: passedEntity remains unmanaged, but newEntity will be managedEntity newEntity = em.merge(passedEntity);
//PERSIST: passedEntity will be managed after thisem.persist(passedEntity);

D2。如果您删除一个实体,然后决定将该实体持久化回来,您只能使用持久化()来执行此操作,因为merge会抛出IllegalArgumentException

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

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

浏览答案时,缺少一些关于“Cascade”和id生成的细节。见问题

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

规格是你的朋友;)

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

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

要理解这一切是如何工作的,你首先应该知道Hibernate将开发人员的思维模式从SQL语句转变为实体状态转换。

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

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

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

JPA实体状态转换

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

Hibernate实体状态转换

如上图所示,一个实体可以处于以下四种状态之一:

  • 新(瞬态)

新创建的对象从未与HibernateSession(a. k. aPersistence Context)关联并且未映射到任何数据库表行,则被视为处于New(瞬态)状态。

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

  • 持久(已管理)

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

  • 已删除

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

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

  • 重新连接

    Hibernate(但不是JPA 2.1)支持通过会话#更新方法重新附加。

    Hibernate会话只能为给定的数据库行关联一个实体对象。这是因为持久性上下文充当内存缓存(一级缓存),并且只有一个值(实体)与给定的键(实体类型和数据库标识符)相关联。

    只有当没有其他JVM对象(匹配相同的数据库行)已经与当前Hibernate会话关联时,才能重新附加实体。

  • 合并

    合并将把分离的实体状态(源)复制到托管实体实例(目标)。如果合并实体在当前会话中没有等效实体,则将从数据库中获取一个。

    即使在合并操作之后,分离的对象实例也将继续保持分离。

  • 删除

    尽管JPA要求只允许删除托管实体,但Hibernate也可以删除分离的实体(但只能通过会话#删除方法调用)。

    删除的实体仅计划删除,实际的数据库DELETE语句将在会话刷新时执行。

我发现Hibernate文档中的这个解释很有启发性,因为它们包含一个用例:

对于新用户来说,合并()的用法和语义学似乎令人困惑。首先,只要你不试图在另一个新的实体管理器中使用加载在一个实体管理器中的对象状态,你应该根本不需要使用合并()。一些整个应用程序永远不会使用这种方法。

通常在以下场景中使用合并():

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

这是合并()的确切语义:

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

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

应将持久化(实体)与全新的实体一起使用,以将它们添加到数据库中(如果数据库中已经存在实体,则会抛出EntityExistsException)。

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

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

JPA无疑是企业领域的一个巨大简化建立在Java平台上的应用程序。作为一个不得不处理J2EE中旧实体bean的复杂性将JPA纳入JavaEE规范是一个巨大的飞跃然而,在深入研究JPA细节时,我发现事情不是那么容易。在这篇文章中,我处理比较EntityManager的合并和持久化方法行为不仅会给新手带来困惑。而且我提出一个泛化,将这两种方法都视为更通用的方法组合。

持久性实体

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

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

在此处输入图片描述

规范更详细,然而,记住它们并不重要,因为这些细节或多或少只涵盖了异国情调的情况。

合并实体

与持久化相比,对合并行为的描述并不那么简单。没有主场景,就像持久化的情况一样,程序员必须记住所有场景才能编写正确的代码。在我看来,JPA设计者想要有一种方法,其主要关注的是处理分离的实体(与主要处理新创建实体的持久化方法相反)。合并方法的主要任务是在持久化上下文中将状态从非托管实体(作为参数传递)传输到其托管对应物。然而,这项任务进一步分为几个场景,这会降低整个方法行为的可理解性。

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

在此处输入图片描述

那么,我应该什么时候使用坚持,什么时候合并?

持续

  • 您希望该方法始终创建一个新实体并且从不更新实体。否则,该方法会抛出异常作为主键唯一性冲突的结果。
  • 批处理进程,以有状态的方式处理实体(请参阅网关模式)。
  • 性能优化

合并

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

您可能来这里是为了获得何时使用持续和何时使用合并的建议。我认为这取决于具体情况:您需要创建新记录的可能性有多大以及检索持久数据的难度有多大。

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

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

    entityManager.persist(实体)

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

  • 持久化的数据需要更新,但是偶尔会有数据还没有记录。在这种情况下,你会查找它,如果缺少实体,请执行持久化:

    实体=entityManager.find(键);

    if(实体==null){entityManager.persist(实体);}

    /*合并 */ }

如果你没有自然键/标识符,你将很难弄清楚实体是否存在,或者如何查找它。

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

  1. 如果更改通常很小,则将其应用于托管实体。
  2. 如果更改很常见,则从持久化实体复制ID以及未更改的数据。然后调用EntityManager::合并()来替换旧内容。

另一个观察:

merge()将只关心一个自动生成的id(在IDENTITYSEQUENCE上测试过),当表中已经存在这样的id时。在这种情况下,merge()将尝试更新记录。但是,如果id不存在或不匹配任何现有记录,merge()将完全忽略它并要求db分配一个新记录。这有时会导致很多错误。不要使用merge()强制为新记录设置id。

另一方面,persist()永远不会让你向它传递id。它会立即失败。在我的情况下,它是:

原因:org.hibernate.持久对象异常:分离的实体传递给持久

hibernate-jpa javadoc有一个提示:

投掷:javax.persistence.EntityExistsException-如果实体已经存在。(如果实体已经存在,则EntityExistsException可能会在持久化操作为调用,或EntityExistsException或另一个持久性异常可以在刷新或提交时抛出。)

Merge不会更新传递的实体,除非该实体被管理。即使实体ID设置为现有的数据库记录,也会在数据库中创建一条新记录。