204

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

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

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

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

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

4

18 回答 18

189

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

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

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

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

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

从关于 JPA 的类似问题的数量来看,似乎即使 JPA 确实声称有一个连贯的模型,它也肯定与大多数程序员的心智模型不匹配,他们被诅咒浪费很多时间试图理解如何获得JPA 做最简单的事情,并最终在其应用程序中使用缓存管理代码。

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

米克

于 2010-12-14T10:50:32.087 回答
36

所有这些答案都忽略了一个重要的区别。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() ...

于 2012-05-17T21:26:01.277 回答
24

实体状态

JPA 定义了以下实体状态:

新(瞬态)

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

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

持久(托管)

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

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

分离式

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

实体状态转换

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

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

JPA 实体状态转换

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

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

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

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

一旦存在托管实体,JPA 会将分离实体的状态复制到当前托管的实体上,并且在 Persistence Context 期间flush,如果脏检查机制发现托管实体已更改,则会生成 UPDATE。

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

重新附加分离的实体

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

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

仅当没有其他 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方法需要你unwrapEntityManager一个 Hibernate Session

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

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

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

当心 NonUniqueObjectException

可能发生的一个问题update是,如果持久性上下文已经包含一个具有相同 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 将抛出 aNonUniqueObjectException因为第二个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 语句,从而减少批量更新执行时间。

于 2020-03-12T20:02:18.003 回答
19

不礼貌的回答:您可能正在寻找扩展的持久性上下文。这是Seam 框架背后的主要原因之一……如果您特别在 Spring 中难以使用 Hibernate,请查看Seam 的这篇文档。

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

于 2009-05-29T00:41:17.397 回答
16

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

session.lock(entity, LockMode.NONE);

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

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

使用 get 也可以工作,除非你得到继承映射(这已经在 getId() 上抛出异常)。

entity = session.get(entity.getClass(), entity.getId());
于 2010-09-10T09:04:06.607 回答
10

我回到 JavaDocorg.hibernate.Session并发现以下内容:

瞬态实例可以通过调用save()persist()来 持久化saveOrUpdate()。持久实例可以通过调用delete(). get()orload()方法返回的任何实例都是持久的。可以通过调用 、 或 使分离的实例update()持久saveOrUpdate()化。瞬态或分离实例的状态也可以通过调用.lock()replicate()merge()

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

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

saveOrUpdate(): 保存或更新

lock(): 已弃用

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

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

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

于 2012-12-15T14:43:13.550 回答
7

我用 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;
}
于 2012-08-21T07:02:08.177 回答
4

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

于 2014-07-15T00:06:58.197 回答
3

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

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);
}
于 2012-05-17T20:52:26.720 回答
2

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

使用 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;

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

于 2013-11-24T06:44:17.257 回答
2

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

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

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

em.refresh(obj)
于 2015-10-25T18:15:48.997 回答
1

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

于 2009-05-28T05:58:48.397 回答
1

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

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

if (session.contains(obj))
{
    session.update(obj);
}
else 
{
    session.merge(obj);
}
于 2011-02-24T18:17:19.440 回答
1

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

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

Example :
    Lot objAttach = em.merge(oldObjDetached);
    objAttach.setEtat(...);
    em.persist(objAttach);
于 2016-10-19T13:43:18.043 回答
0

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

于 2011-01-05T16:45:41.887 回答
0

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

在休眠 5.4.9 上测试

SessionFactoryOptionsBuilder

于 2019-12-11T11:44:07.680 回答
-1

Hibernate 支持通过多种方式重新附加分离的实体,请参阅Hibernate 用户指南

于 2019-11-01T03:06:35.000 回答
-6
try getHibernateTemplate().saveOrUpdate()
于 2009-05-26T21:01:15.637 回答