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

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

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

无论哪种方式将一个实体添加到一个PersistenceContext,不同之处在于你之后对实体做什么。

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

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

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

 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) 

scheme1和scheme3大致相同,但在某些情况下,您希望使用scheme2。

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

(编辑扩大差异信息)

坚持:

  • 向数据库插入一个新的寄存器
  • 将该对象附加到实体pipe理器。

合并:

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

坚持()效率:

  • 插入一个新的寄存器到数据库可能比merge()更有效率。
  • 它不会复制原始对象。

persist()语义:

  • 它确保你插入,而不是错误更新。

例:

 { 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); } 

这种方式只对实体pipe理器中的任何寄存器存在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 merge()可以像使用ON DUPLICATE KEY UPDATE选项对INSERT的调用一样有效,persist(),JPA是一个非常高层次的编程,你不能认为这将是事实。

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

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

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

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

hibernate监视当前连接的实体。 但是对于一个实体的pipe理,它必须处于正确的实体状态。

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

  • 新(瞬态)

    一个新创build的对象还没有被关联到一个hibernateSession (又名Persistence Context ),并没有被映射到任何数据库表行被认为是在新(瞬态)状态。

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

  • 持久(pipe理)

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

  • 超脱

    一旦当前正在运行的持久性上下文被closures,所有先前pipe理的实体将被分离。 连续的更改将不再被跟踪,也不会自动进行数据库同步。

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

    • 重新连接

      Hibernate(但不是JPA 2.1)支持通过Session#更新方法重新连接。 Hibernate Session只能将一个Entity对象关联给给定的数据库行。 这是因为持久化上下文充当内存中caching(一级caching),并且只有一个值(实体)与给定键(实体types和数据库标识符)关联。 只有当没有其他与当前Hibernate Session相关的JVM对象(匹配相同的数据库行)时才能重新连接实体。

    • 合并

    合并将把分离的实体状态(源)复制到pipe理实体实例(目的地)。 如果合并实体在当前会话中没有等价物,那么将从数据库中提取合并实体。 即使在合并操作之后,分离的对象实例仍将保持分离状态。

  • 删除

    尽pipeJPA要求只允许删除托pipe实体,但Hibernate也可以删除分离的实体(但只能通过Session#delete方法调用)。 一个被删除的实体只被安排删除,实际的数据库DELETE语句将在会话刷新时被执行。

为了更好地理解JPA状态转换,可以看到下图:

在这里输入图像描述

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

在这里输入图像描述

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

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

如果X是分离的对象,则在调用persist操作时可能会引发EntityExistsException或者在刷新或提交时抛出EntityExistsException或其他PersistenceException

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

尽pipe规范不清楚 , persist()可能会为对象设置@GeneratedValue @Idmerge()然而必须有已经生成的@Id的对象。

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

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

merge()在新实体上调用时,其行为与persist()操作类似。 它将实体添加到持久化上下文中,而不是添加原始实体实例,而是创build一个新副本并pipe理该实例。 由merge()操作创build的副本将被持久化,就好像调用了persist()方法一样。

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

延迟加载关系是合并操作中的特例。 如果一个实体在分离之前没有触发延迟加载关系,那么实体合并时该关系将被忽略。 如果关系在托pipe时被触发,然后在实体分离时设置为空,那么实体的托pipe版本也将在合并期间清除关系。

以上所有信息均来自Mike Keith和Merrick Schnicariol的“专业JPA 2掌握Java™持久性API”。 第6章支队和合并。 这本书实际上是第二本由作者致力于JPA的书。 这本新书有许多新的信息,然后是前者。 我真的build议阅读这本书,那些将认真参与JPA的人。 我很抱歉发表我的第一个答案。

mergepersist之间还有一些区别(我会再次列举已经发布在这里的):

D1。 merge不会使传递的实体进行pipe理,而是返回另一个受pipe理的实例。 persist在对方将通过实体pipe理:

 //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

我得到了lazyLoading在我的实体exception,因为我试图访问在会话中的一个懒加载的集合。

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

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

以下为我工作:

 // 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!! 

通过答案有一些细节丢失有关`级联'和身份证代。 看问题

此外,值得一提的是,您可以分别使用Cascade注释进行合并和保存: Cascade.MERGECascade.PERSIST ,它们将根据使用的方法进行处理。

规范是你的朋友;)

我从Hibernate文档中find了这个解释,因为它们包含一个用例:

merge()的用法和语义似乎让新用户感到困惑。 首先,只要你不试图在另一个新的实体pipe理器中使用一个实体pipe理器中加载的对象状态,你根本不需要使用merge() 。 一些完整的应用程序将永远不会使用此方

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

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

这是merge()的确切语义:

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

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

情景X:

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

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

  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!

  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!! 

在Java平台上构build的企业应用程序领域,JPA毫无疑问是一个极大的简化。 作为一个开发人员,他不得不应对J2EE中旧的实体bean的复杂性,我认为在Java EE规范中包含JPA是一个很大的飞跃。 但是,在深入研究JPA细节的同时,我发现并不那么容易。 在本文中,我将处理EntityManager的merge和persist方法的比较,这些方法的重叠行为不仅会给新手造成混淆。 此外,我提出一个泛化,认为这两种方法是一个更一般的方法结合的特殊情况。

坚持实体

与合并方法相比,persist方法非常简单直观。 坚持使用方法最常见的情况可以概括如下:

“一个新创build的实体类的实例被传递给persist方法,在这个方法返回之后,这个实体被pipe理并计划插入到数据库中,可能发生在事务提交之前或者在事务提交之前或者flush方法被调用的时候。如果实体通过标记为PERSIST级联策略的关系引用另一个实体,则此过程也适用于此。

在这里输入图像描述

规范更多的是细节,但是,记住这些细节并不重要,因为这些细节只包含或多或less的异国情调。

合并实体

相比之下,合并行为的描述并不那么简单。 没有主要的场景,因为它是持续的,程序员必须记住所有场景才能写出正确的代码。 在我看来,JPAdevise者想要有一些方法,主要关注的是处理分离的实体(与主要处理新创build实体的持久化方法相反)。合并方法的主要任务是将状态从非托pipe实体(作为parameter passing给持久化上下文中的受pipe对象)。 然而,这个任务进一步分解成几个情况,使整个方法的行为的可理解性恶化。

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

在这里输入图像描述

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

坚持

  • 你想要的方法总是创build一个新的实体,永远不会更新一个实体。 否则,由于主键唯一性违反,该方法抛出exception。
  • 批处理,以有状态方式处理实体(请参阅网关模式)。
  • 性能优化

合并

  • 您希望该方法插入或更新数据库中的实体。
  • 您希望以无状态的方式处理实体(服务中的数据传输对象)
  • 你想插入一个新的实体,可能有一个可能但可能还没有创build的另一个实体的引用(关系必须标记为MERGE)。 例如,插入一张新的照片,以引用新的或已有的专辑。

persist(entity)应该与全新实体一起使用,将它们添加到数据库中(如果实体已经存在于数据库中,则会出现EntityExistsException throw)。

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

可能persist正在生成INSERT sql语句并合并更新sql语句(但我不知道)。