ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

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


m
mikhailfranco

因此,似乎没有办法在 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 节中找到
持久对象工作得很好,脏标志是根据初始加载和 flush() 时间的值之间的增量设置的。分离的对象需要,但目前没有此功能。休眠的方法是为分离的对象添加一个额外的哈希/ID。并保留可用分离对象的最后状态的快照,就像它们对持久对象所做的那样。因此,他们可以利用所有现有代码并使其适用于分离的对象。正如@mikhailfranco 所指出的那样,我们不会“将陈旧状态推送到数据库,并覆盖任何干预更新”
根据 Hibernate javadoc(但不是 JPA),lock(LockMode.NONE) 实际上可以在瞬态对象上调用,并且它确实将实体重新附加到会话。请参阅stackoverflow.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) 在 com.github.vok.framework.DisableTransactionControlEMDelegate.lock(DB.kt)
N
Nathan Hughes

所有这些答案都忽略了一个重要的区别。 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,并对其进行更新。
V
Vlad Mihalcea

实体状态

JPA 定义了以下实体状态:

新(瞬态)

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

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

持久(托管)

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

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

分离式

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

实体状态转换

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

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

https://i.stack.imgur.com/Hez6p.png

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

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

https://i.stack.imgur.com/CpfAe.png

合并分离的实体

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

假设我们已经持久化了以下 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 期间,如果脏检查机制发现托管实体已更改,则将生成 UPDATE。

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

重新附加分离的实体

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

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

仅当没有其他 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 方法要求您将 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 实体,其标识符与我们传递给 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 还是无关紧要(如果 hibernate 在刷新之前在一次调用中获取所有带注释的实体可能很重要)?
@SelectBeforeUpdate 在持久性上下文 flush 操作期间触发 SELECT。查看 the getDatabaseSnapshot method in the DefaultFlushEntityEventListener 了解更多详情。
如果您想同时保存或更新并避免 NonUniqueObjectException 怎么办?如果实体在那里,您想要更新上下文,否则创建一个新的。你必须手动做还是有办法?
合并解决了这个问题。这是最简单的解决方法。
c
cwash

平淡无奇的回答:您可能正在寻找扩展的持久性上下文。这是 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”也已失效。
J
John Rizzo

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

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)
r
rtruszk

我回到 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() 不应直接使用,并且可以根据功能要求选择其中的一个或多个。


r
rtruszk

我用 NHibernate 在 C# 中这样做了,但它在 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();
        }
    }
}

对每个对象都调用了 First Lock,因为 Contains 始终为 false。问题是 NHibernate 按数据库 id 和类型比较对象。 Contains 使用 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;
}

d
djmj

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 的少数几个方面之一,关于更改的实体及其属性的不同附加选项。


W
WhoopP

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

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

G
Gwaptiva

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

使用 Hibernate 3.5.0-Final

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

所以,我的附加方法看起来有点像

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

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


H
Hartmut Pfarr

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

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

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

em.refresh(obj)

P
Pavitar Singh

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


M
Marc-Andre

在原始帖子中,提到了两种方法 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 永远不会在您的代码中被调用。
R
Ryuku

要重新附加此对象,您必须使用 merge();

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

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

c
cheesus

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


s
slideshowp2

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

在休眠 5.4.9 上测试

SessionFactoryOptionsBuilder


B
Baby Groot
try getHibernateTemplate().saveOrUpdate()