在 Hibernate 中重新附加分离对象的正确方法是啥?

Posted

技术标签:

【中文标题】在 Hibernate 中重新附加分离对象的正确方法是啥?【英文标题】:What is the proper way to re-attach detached objects in Hibernate?在 Hibernate 中重新附加分离对象的正确方法是什么? 【发布时间】:2010-10-29 02:22:49 【问题描述】:

我有一种情况,我需要将分离的对象重新附加到休眠会话,尽管会话中可能已经存在相同标识的对象,这会导致错误。

现在,我可以做两件事之一。

    getHibernateTemplate().update( obj ) 当且仅当对象在休眠会话中不存在时才有效。当我以后需要它时,会抛出异常,说明会话中已经存在具有给定标识符的对象。

    getHibernateTemplate().merge( obj ) 当且仅当休眠会话中存在对象时,此方法才有效。如果我稍后需要对象在会话中,如果我使用它,则会引发异常。

鉴于这两种情况,我一般如何将会话附加到对象?我不想使用异常来控制这个问题的解决方案的流程,因为必须有一个更优雅的解决方案......

【问题讨论】:

【参考方案1】:

所以似乎没有办法在 JPA 中重新附加过时的分离实体。

merge() 会将陈旧状态推送到数据库, 并覆盖任何介入的更新。

refresh() 不能在分离的实体上调用。

lock() 不能在分离的实体上调用, 即使它可以,而且它确实重新连接了实体, 使用参数“LockMode.NONE”调用“lock” 暗示你正在锁定,但没有锁定, 是我见过的最违反直觉的 API 设计。

所以你被卡住了。 有一个detach() 方法,但没有attach()reattach()。 您无法使用对象生命周期中的明显步骤。

从关于 JPA 的类似问题的数量来看, 似乎即使 JPA 确实声称有一个连贯的模型, 它肯定不符合大多数程序员的心理模型, 被诅咒浪费许多时间试图理解的人 如何让 JPA 做最简单的事情,最终得到缓存 应用程序中的所有管理代码。

似乎唯一的方法就是丢弃你陈旧的分离实体 并使用相同的 id 进行查找查询,这将命中 L2 或 DB。

米克

【讨论】:

我想知道 JPA 规范是否有理由不允许 refresh() 用于分离实体?纵观 2.0 规范,我看不出任何理由;只是它是不允许的。 这绝对不准确。来自 JPwH:*Reattaching a modified detached instance* A detached instance may be reattached to a new Session (and managed by this new persistence context) by calling update() on the detached object. In our experience, it may be easier for you to understand the following code if you rename the update() method in your mind to reattach()—however, there is a good reason it’s called updating. 更多内容见第 9.3.2 节 持久对象工作得很好,脏标志是根据初始加载和刷新()时间的值之间的增量设置的。分离的对象需要,但目前没有此功能。休眠的方法是为分离的对象添加一个额外的哈希/ID。并保留可用分离对象的最后状态的快照,就像它们对持久对象所做的那样。因此,他们可以利用所有现有代码并使其适用于分离的对象。正如@mikhailfranco 指出的那样,我们不会“将陈旧状态推送到数据库,并覆盖任何干预更新” 根据 Hibernate javadoc(但不是 JPA),lock(LockMode.NONE) 实际上可以在瞬态对象上调用,并且它确实将实体重新附加到会话。见***.com/a/3683370/14379 锁对我不起作用:java.lang.IllegalArgumentException: entity not in the persistence context at org.hibernate.internal.SessionImpl.lock(SessionImpl.java:3491) at org.hibernate。 internal.SessionImpl.lock(SessionImpl.java:3482) at com.github.vok.framework.DisableTransactionControlEMDelegate.lock(DB.kt)【参考方案2】:

所有这些答案都忽略了一个重要的区别。 update() 用于(重新)将您的对象图附加到会话。您传递给它的对象是被管理的对象。

merge() 实际上不是一个(重新)附件 API。注意 merge() 有返回值吗?那是因为它会返回托管图,它可能不是您传递给它的图。 merge() 是一个 JPA API,其行为受 JPA 规范的约束。如果您传递给 merge() 的对象已经被管理(已经与会话相关联),那么这就是 Hibernate 使用的图形;传入的对象与从 merge() 返回的对象相同。但是,如果您传递给 merge() 的对象是分离的,Hibernate 会创建一个新的托管对象图,并将状态从您的分离图复制到新的托管图上。同样,这一切都由 JPA 规范规定和管理。

就“确保此实体受管理或使其受管理”的通用策略而言,这在某种程度上取决于您是否还想考虑尚未插入的数据。假设你这样做,使用类似的东西

if ( session.contains( myEntity ) ) 
    // nothing to do... myEntity is already associated with the session

else 
    session.saveOrUpdate( myEntity );

请注意,我使用了 saveOrUpdate() 而不是 update()。如果您不想在此处处理尚未插入的数据,请改用 update()...

【讨论】:

这是这个问题的正确答案 - 案件已结案! Session.contains(Object) 通过引用检查。如果会话中已经有另一个实体表示同一行并且您传递了一个分离的实例,您将得到一个异常。 Session.contains(Object) 通过引用检查,如果有 另一个 实体代表会话中的同一行,它将返回 false,并会更新它。【参考方案3】:

实体状态

JPA 定义了以下实体状态:

新建(瞬态)

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

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

持久(托管)

持久化实体已与数据库表行相关联,并由当前运行的持久化上下文管理。对此类实体所做的任何更改都将被检测到并传播到数据库(在会话刷新期间)。

使用 Hibernate,我们不再需要执行 INSERT/UPDATE/DELETE 语句。 Hibernate 采用事务性后写工作方式,并且在当前Sessionflush-time 期间的最后一个负责时刻同步更改。

分离

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

实体状态转换

您可以使用EntityManager 接口定义的各种方法更改实体状态。

要更好地理解 JPA 实体状态转换,请考虑下图:

使用 JPA 时,要将分离的实体重新关联到活动的 EntityManager,您可以使用 merge 操作。

使用原生 Hibernate API 时,除了 merge,您可以使用更新方法将分离的实体重新附加到活动的 Hibernate 会话,如下图所示:

合并一个分离的实体

合并会将分离的实体状态(源)复制到托管实体实例(目标)。

假设我们已经持久化了以下 Book 实体,现在该实体已分离,因为用于持久化实体的 EntityManager 已关闭:

Book _book = doInJPA(entityManager -> 
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");
 
    entityManager.persist(book);
 
    return book;
);

实体处于分离状态时,我们修改如下:

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

现在,我们要将更改传播到数据库,因此我们可以调用merge 方法:

doInJPA(entityManager -> 
    Book book = entityManager.merge(_book);
 
    LOGGER.info("Merging the Book entity");
 
    assertFalse(book == _book);
);

Hibernate 将执行以下 SQL 语句:

SELECT
    b.id,
    b.author AS author2_0_,
    b.isbn AS isbn3_0_,
    b.title AS title4_0_
FROM
    book b
WHERE
    b.id = 1
 
-- Merging the Book entity
 
UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

如果合并实体在当前EntityManager 中没有等价物,则会从数据库中获取新的实体快照。

一旦存在托管实体,JPA 将分离实体的状态复制到当前托管的实体上,并且在持久性上下文flush 期间,如果脏检查机制发现托管实体将生成更新变了。

因此,当使用merge 时,即使在合并操作之后,分离的对象实例仍将继续保持分离状态。

重新附加分离的实体

Hibernate,但 JPA 不支持通过 update 方法重新附加。

Hibernate Session 只能为给定的数据库行关联一个实体对象。这是因为 Persistence Context 充当内存缓存(一级缓存),只有一个值(实体)与给定的键(实体类型和数据库标识符)相关联。

只有在没有其他 JVM 对象(匹配同一数据库行)与当前 Hibernate Session 关联时,才能重新附加实体。

考虑到我们已经持久化了 Book 实体,并且我们在 Book 实体处于分离状态时对其进行了修改:

Book _book = doInJPA(entityManager -> 
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");
 
    entityManager.persist(book);
 
    return book;
);
      
_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);

我们可以像这样重新附加分离的实体:

doInJPA(entityManager -> 
    Session session = entityManager.unwrap(Session.class);
 
    session.update(_book);
 
    LOGGER.info("Updating the Book entity");
);

而Hibernate会执行如下SQL语句:

-- Updating the Book entity
 
UPDATE
    book
SET
    author = 'Vlad Mihalcea',
    isbn = '978-9730228236',
    title = 'High-Performance Java Persistence, 2nd edition'
WHERE
    id = 1

update 方法要求您将unwrap EntityManager 转换为 Hibernate Session

merge 不同,提供的分离实体将与当前持久性上下文重新关联,并且无论实体是否已修改,都会在刷新期间安排更新。

为防止这种情况,您可以使用@SelectBeforeUpdate Hibernate 注释,该注释将触发一个 SELECT 语句,该语句获取加载状态,然后由脏检查机制使用。

@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book 
 
    //Code omitted for brevity

注意 NonUniqueObjectException

update 可能出现的一个问题是,如果 Persistence Context 已经包含具有相同 id 和相同类型的实体引用,如下例所示:

Book _book = doInJPA(entityManager -> 
    Book book = new Book()
    .setIsbn("978-9730228236")
    .setTitle("High-Performance Java Persistence")
    .setAuthor("Vlad Mihalcea");
 
    Session session = entityManager.unwrap(Session.class);
    session.saveOrUpdate(book);
 
    return book;
);
 
_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);
 
try 
    doInJPA(entityManager -> 
        Book book = entityManager.find(
            Book.class,
            _book.getId()
        );
 
        Session session = entityManager.unwrap(Session.class);
        session.saveOrUpdate(_book);
    );
 catch (NonUniqueObjectException e) 
    LOGGER.error(
        "The Persistence Context cannot hold " +
        "two representations of the same entity",
        e
    );

现在,在执行上面的测试用例时,Hibernate 将抛出一个NonUniqueObjectException,因为第二个EntityManager 已经包含一个Book entity,其标识符与我们传递给update 的标识符相同,并且持久性上下文不能包含同一实体的两种表示。

org.hibernate.NonUniqueObjectException:
    A different object with the same identifier value was already associated with the session : [com.vladmihalcea.book.hpjp.hibernate.pc.Book#1]
    at org.hibernate.engine.internal.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:651)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:284)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:227)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:92)
    at org.hibernate.event.internal.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:73)
    at org.hibernate.internal.SessionImpl.fireSaveOrUpdate(SessionImpl.java:682)
    at org.hibernate.internal.SessionImpl.saveOrUpdate(SessionImpl.java:674)

结论

如果您使用乐观锁定,则首选merge 方法,因为它可以防止丢失更新。

update 适合批量更新,因为它可以防止merge 操作生成额外的 SELECT 语句,从而减少批量更新执行时间。

【讨论】:

不错的答案。不过,我想知道 @SelectBeforeUpdate 注释。什么时候触发选择?在刷新之前调用update,或者它并不重要(如果休眠在刷新之前在一次调用中获取所有带注释的实体可能很重要)? @SelectBeforeUpdate 在 Persistence Context flush 操作期间触发 SELECT。查看the getDatabaseSnapshot method in the DefaultFlushEntityEventListener了解更多详情。 如果您想同时保存或更新并避免 NonUniqueObjectException 怎么办?如果实体在那里,您想要更新上下文,否则创建一个新的。是要手动还是有办法? 合并解决了这个问题。这是最简单的解决方法。【参考方案4】:

平淡无奇的回答:您可能正在寻找扩展的持久性上下文。这是 Seam Framework 背后的主要原因之一...如果您特别在 Spring 中难以使用 Hibernate,请查看 Seam 文档的 this piece。

外交回答:这在Hibernate docs 中有描述。如果您需要更多说明,请查看 Java Persistence with Hibernate 的第 9.3.2 节,名为“使用分离的对象”。我强烈建议您阅读这本书,如果您使用 Hibernate 做的不仅仅是 CRUD。

【讨论】:

来自seamframework.org:“Seam 3 的积极开发已被 Red Hat 停止。”链接“this piece of Seam's docs”也已失效。【参考方案5】:

如果您确定您的实体没有被修改(或者如果您同意任何修改都将丢失),那么您可以将其重新附加到带有锁定的会话。

session.lock(entity, LockMode.NONE);

它不会锁定任何东西,但它会从会话缓存中获取实体,或者(如果没有找到)从数据库中读取它。

当您从“旧”(例如来自 HttpSession)实体导航关系时,防止 LazyInitException 非常有用。您首先“重新附加”实体。

使用 get 也可以工作,除非您得到继承映射(这将在 getId() 上引发异常)。

entity = session.get(entity.getClass(), entity.getId());

【讨论】:

我想将实体与会话重新关联。不幸的是,Session.lock(entity, LockMode.NONE) 失败,异常说:无法重新关联未初始化的瞬态集合。如何克服这一点? 事实上我并不完全正确。使用 lock() 重新附加您的实体,但不会重新附加绑定到它的其他实体。因此,如果您执行 entity.getOtherEntity().getYetAnotherEntity(),您可能会遇到 LazyInit 异常。我知道克服这个问题的唯一方法是使用 find。 entity = em.find(entity.getClass(), entity.getId(); 没有Session.find() API 方法。也许你的意思是Session.load(Object object, Serializable id)【参考方案6】:

我回到 org.hibernate.Session 的 JavaDoc 并找到以下内容:

通过调用save()persist()saveOrUpdate()。可以通过调用delete() 将持久实例变为瞬态。 get()load() 方法返回的任何实例都是持久的。可以通过调用update()saveOrUpdate()lock()replicate() 使分离的实例持久化。通过调用merge(),也可以将瞬态或分离实例的状态作为新的持久实例持久化。

因此update()saveOrUpdate()lock()replicate()merge() 是候选选项。

update():如果存在具有相同标识符的持久实例,则会抛出异常。

saveOrUpdate(): 要么保存要么更新

lock():已弃用

replicate():保持给定分离实例的状态,重用当前标识符值。

merge():返回具有相同标识符的持久对象。给定实例未与会话关联。

因此,lock() 不应直接使用,可以根据功能需求选择其中的一个或多个。

【讨论】:

【参考方案7】:

我在 C# 中使用 NHibernate 就是这样做的,但它在 Java 中应该以同样的方式工作:

public virtual void Attach()

    if (!HibernateSessionManager.Instance.GetSession().Contains(this))
    
        ISession session = HibernateSessionManager.Instance.GetSession();
        using (ITransaction t = session.BeginTransaction())
        
            session.Lock(this, NHibernate.LockMode.None);
            t.Commit();
        
    

对每个对象都调用了第一个锁,因为 Contains 始终为 false。问题是 NHibernate 按数据库 id 和类型比较对象。包含使用equals 方法,如果它没有被覆盖,它会通过引用进行比较。使用 equals 方法,它可以正常工作,没有任何异常:

public override bool Equals(object obj)

    if (this == obj)  
        return true;
     
    if (GetType() != obj.GetType()) 
        return false;
    
    if (Id != ((BaseObject)obj).Id)
    
        return false;
    
    return true;

【讨论】:

【参考方案8】:

Session.contains(Object obj) 检查引用,不会检测到表示同一行且已附加到该行的不同实例。

这是我对具有标识符属性的实体的通用解决方案。

public static void update(final Session session, final Object entity)

    // if the given instance is in session, nothing to do
    if (session.contains(entity))
        return;

    // check if there is already a different attached instance representing the same row
    final ClassMetadata classMetadata = session.getSessionFactory().getClassMetadata(entity.getClass());
    final Serializable identifier = classMetadata.getIdentifier(entity, (SessionImplementor) session);

    final Object sessionEntity = session.load(entity.getClass(), identifier);
    // override changes, last call to update wins
    if (sessionEntity != null)
        session.evict(sessionEntity);
    session.update(entity);

这是我喜欢的 .Net EntityFramework 的少数几个方面之一,关于更改的实体及其属性的不同附加选项。

【讨论】:

【参考方案9】:

我想出了一个解决方案,从持久存储中“刷新”一个对象,该对象将解释可能已经附加到会话的其他对象:

public void refreshDetached(T entity, Long id)

    // Check for any OTHER instances already attached to the session since
    // refresh will not work if there are any.
    T attached = (T) session.load(getPersistentClass(), id);
    if (attached != entity)
    
        session.evict(attached);
        session.lock(entity, LockMode.NONE);
    
    session.refresh(entity);

【讨论】:

【参考方案10】:

抱歉,似乎无法添加 cmets(还没有?)。

使用 Hibernate 3.5.0-Final

虽然Session#lock 方法已被弃用,但javadoc 确实 建议使用Session#buildLockRequest(LockOptions)#lock(entity),如果您确保您的关联具有cascade=lock,则延迟加载也不是问题。

所以,我的 attach 方法有点像

MyEntity attach(MyEntity entity) 
    if(getSession().contains(entity)) return entity;
    getSession().buildLockRequest(LockOptions.NONE).lock(entity);
    return entity;

初步测试表明它是一种享受。

【讨论】:

【参考方案11】:

也许它在 Eclipselink 上的行为略有不同。为了在不获取过时数据的情况下重新附加分离的对象,我通常会这样做:

Object obj = em.find(obj.getClass(), id);

作为可选的第二步(使缓存失效):

em.refresh(obj)

【讨论】:

【参考方案12】:

尝试 getHibernateTemplate().replicate(entity,ReplicationMode.LATEST_VERSION)

【讨论】:

【参考方案13】:

在原始帖子中,有两种方法,update(obj)merge(obj) 被提及工作,但在相反的情况下。如果这真的是真的,那为什么不先测试一下对象是否已经在会话中,如果是则调用update(obj),否则调用merge(obj)

会话中存在的测试是session.contains(obj)。因此,我认为以下伪代码会起作用:

if (session.contains(obj))

    session.update(obj);

else 

    session.merge(obj);

【讨论】:

contains() 通过引用检查比较,但休眠函数按数据库 ID 工作。 session.merge 永远不会在你的代码中被调用。【参考方案14】:

要重新附加这个对象,你必须使用merge();

此方法在参数中接受您的实体分离并返回一个实体将被附加并从数据库重新加载。

Example :
    Lot objAttach = em.merge(oldObjDetached);
    objAttach.setEtat(...);
    em.persist(objAttach);

【讨论】:

【参考方案15】:

调用第一个 merge()(更新持久实例),然后调用 lock(LockMode.NONE)(附加当前实例,而不是 merge() 返回的实例)似乎适用于某些用例。

【讨论】:

【参考方案16】:

hibernate.allow_refresh_detached_entity 属性为我解决了问题。但这是一个通用规则,所以如果你只想在某些情况下这样做,它不是很合适。希望对你有帮助。

在 Hibernate 5.4.9 上测试

SessionFactoryOptionsBuilder

【讨论】:

【参考方案17】:

Hibernate 支持通过多种方式重新附加分离的实体,请参阅Hibernate user guide。

【讨论】:

【参考方案18】:
try getHibernateTemplate().saveOrUpdate()

【讨论】:

以上是关于在 Hibernate 中重新附加分离对象的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

linux虚拟内存附加/分离信号

实体框架 - 分离和重新附加实体?

散列更改时分离和重新附加元素

确定 Hibernate 中哪个类拥有方

tomcat 7.0.42 pooling, hibernate 4.2, mysql 坚如磐石的自动重新连接解决方​​案

分离和附加数据库