首页 文章

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

提问于
浏览
868

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

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

15 回答

  • 1496

    如果您使用的是指定的生成器using merge instead of persist can cause a redundant SQL statement,则会影响性能 .

    此外,calling merge for managed entities也是一个错误,因为管理实体由Hibernate自动管理,并且dirty checking mechanismflushing the Persistence Context时它们的状态与数据库记录同步 .

    要了解这一切是如何工作的,你应该首先知道Hibernate会改变它开发人员的心态从SQL语句到entity state transitions .

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

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

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

    • 新(瞬态)

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

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

    • 持久性(管理)

    持久化实体已与数据库表行关联,并由当前运行的持久性上下文管理 . 对此类实体所做的任何更改都将被检测并传播到数据库(在会话刷新时间内) . 使用Hibernate,我们不再需要执行INSERT / UPDATE / DELETE语句 . Hibernate采用transactional write-behind工作方式,并且在当前 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

  • 28

    另一个观察:

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

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

    引起:org.hibernate.PersistentObjectException:传递给persist的分离实体

    hibernate-jpa javadoc有一个提示:

    抛出:javax.persistence.EntityExistsException - 如果实体已存在 . (如果实体已存在,则在调用persist操作时可能会抛出EntityExistsException,或者可能在flush或commit时抛出EntityExistsException或其他PersistenceException . )

  • 6

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

    如果X是一个分离的对象,则在调用persist操作时可能会抛出EntityExistsException,或者可能在flush或commit时抛出EntityExistsException或另一个PersistenceException .

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

    虽然the specification is unclearpersist() 可能会为对象设置 @GeneratedValue @Id . merge() 但是必须有一个已生成 @Id 的对象 .

  • 122

    场景X:

    表:Spitter(一),表:Spittles(很多)(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!!
    
  • 0

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

    merge()的用法和语义似乎让新用户感到困惑 . 首先,只要您不尝试在另一个新实体管理器中的一个实体管理器中使用加载的对象状态,您根本不需要使用merge() . 一些整个应用程序永远不会使用此方法 . 通常在以下场景中使用merge():应用程序在第一个实体管理器中加载对象,将对象传递给表示层,对对象进行一些修改,将对象传递回应用程序的业务逻辑层通过在第二个实体管理器中调用merge()来持久化这些修改这里是merge()的确切语义:如果存在当前与持久性上下文关联的相同标识符的托管实例,则将给定对象的状态复制到托管实例如果当前没有与持久化上下文关联的托管实例,尝试从数据库加载它,或者创建一个新的托管实例,则返回托管实例,给定实例不会与持久性上下文关联,它仍然是分离的并且是通常被丢弃

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

  • 5

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

  • 155

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

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

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

  • 16

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

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

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

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

    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 .

  • 8

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

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

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

    试试{entityManager.persist(entity)}

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

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

    entity = entityManager.find(key);

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

    别的{/ 合并 /}

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

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

    • 如果更改通常很小,请将它们应用于托管实体 .

    • 如果更改很常见,请复制持久化实体中的ID以及未更改的数据 . 然后调用EntityManager :: merge()来替换旧内容 .

  • 5

    JPA毫无疑问是在Java平台上构建的企业应用程序领域的一个很大的简化 . 作为一个开发人员必须应对J2EE中旧实体bean的复杂性我发现Java EE规范中包含JPA是一个重大飞跃 . 然而,在深入研究JPA细节时,我发现事情并非那么容易 . 在本文中,我将讨论EntityManager的merge和persist方法的比较,这些方法的重叠行为不仅会导致新手混淆 . 此外,我提出了一种概括,即将两种方法视为更通用方法的特殊情况 .

    Persisting entities

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

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

    enter image description here

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

    Merging entities

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

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

    enter image description here

    So, when should I use persist and when merge?

    persist

    • 您希望该方法始终创建一个新实体,并且永远不会更新实体 . 否则,该方法会因主键唯一性违规而抛出异常 .

    • 批处理,以有状态方式处理实体(请参阅网关模式) .

    • 性能优化

    merge

    • 您希望该方法在数据库中插入或更新实体 .

    • 您希望以无状态方式处理实体(服务中的数据传输对象)

    • 您想要插入一个新实体,该实体可能具有对可能但尚未创建的另一个实体的引用(关系必须标记为MERGE) . 例如,插入新照片时引用新的或预先存在的相册 .

  • 37

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

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

    为了缓解这种情况,我在我的控制器中更新了同一个实体并将其传递给了我的jsp,虽然我想当我在会话中重新保存时它也可以通过 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!!
    
  • 1

    通过答案,有关“Cascade”和id生成的一些细节缺失 . See question

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

    规格是你的朋友;)

  • 1

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

    (编辑扩大差异信息)

    坚持:

    • 将新寄存器插入数据库

    • 将对象附加到实体管理器 .

    合并:

    • 查找具有相同ID的附加对象并进行更新 .

    • 如果存在更新并返回已附加的对象 .

    • 如果不存在,则将新寄存器插入数据库 .

    persist()效率:

    • 将新注册表插入数据库比使用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);
    }
    

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

    merge()对于具有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一样有效,但JPA是一个非常高级的编程,你不能认为这种情况在任何地方都是如此 .

  • 15

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

    返回除原始实体之外的托管实例是合并过程的关键部分 . 如果持久性上下文中已存在具有相同标识符的实体实例,则提供程序将使用正在合并的实体的状态覆盖其状态,但必须将已存在的托管版本返回到客户端,以便它可以是用过的 . 如果提供程序未在持久性上下文中更新Employee实例,则对该实例的任何引用都将与要合并的新状态不一致 . 当在新实体上调用merge()时,它的行为与persist()操作类似 . 它将实体添加到持久性上下文中,但它不是添加原始实体实例,而是创建新副本并管理该实例 . merge()操作创建的副本是持久化的,就好像在其上调用了persist()方法一样 . 在存在关系的情况下,merge()操作将尝试更新托管实体以指向分离实体引用的实体的托管版本 . 如果实体与没有持久标识的对象有关系,则合并操作的结果是未定义的 . 某些提供程序可能允许托管副本指向非持久对象,而其他提供程序可能会立即引发异常 . 在这些情况下,可以选择级联merge()操作以防止发生异常 . 我们将在本节后面介绍merge()操作的级联 . 如果要合并的实体指向已删除的实体,则将抛出IllegalArgumentException异常 . 延迟加载关系是合并操作中的一种特殊情况 . 如果在实体分离之前未在实体上触发延迟加载关系,则在合并实体时将忽略该关系 . 如果在托管时触发关系,然后在分离实体时将其设置为null,则实体的托管版本同样会在合并期间清除关系 . “

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

  • 5

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

    应该使用merge(entity),如果实体被分离并被更改,则将实体放回到持久化上下文 .

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

相关问题