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

时间:2009-05-26 20:43:51

标签: java hibernate spring session detach

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

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

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

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

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

18 个答案:

答案 0 :(得分:172)

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

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

refresh()无法在分离的实体上调用。

无法在分离的实体上调用

lock(), 即使它可以,它确实重新附加了实体, 用参数'LockMode.NONE'调用'lock' 暗示你是锁定的,但没有锁定, 是我见过的最具反直觉的API设计。

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

根据有关JPA的类似问题的数量来判断, 似乎即使JPA声称拥有一个连贯的模型, 它肯定与大多数程序员的心理模型不符, 谁被诅咒浪费了很多时间才能理解 如何让JPA做最简单的事情,最终得到缓存 管理代码遍布他们的应用程序。

似乎唯一的方法是丢弃过时的分离实体 并使用相同的ID执行查找查询,该查询将命中L2或DB。

Mik的

答案 1 :(得分:25)

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

merge()实际上不是(重新)附件API。注意merge()有一个返回值?那是因为它会返回托管图,这可能不是您传递的图。 merge()是一个JPA API,其行为受JPA规范的约束。如果您传入merge()的对象已经被管理(已经与Session关联)那么这就是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()代替......

答案 2 :(得分:20)

Undiplomatic answer:您可能正在寻找扩展的持久化上下文。这是Seam Framework背后的主要原因之一...如果您特别想在Spring中使用Hibernate,请查看Seam文档的this piece

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

答案 3 :(得分:16)

由于这是一个非常常见的问题,所以我写了 this article,此答案基于此。

实体状态

JPA定义以下实体状态:

新(瞬态)

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

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

永久(受管理)

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

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

已分离

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

实体状态转换

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

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

JPA entity state transitions

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

使用本地Hibernate API时,除了merge之外,还可以使用update方法将分离的实体重新附加到活动的Hibernate Session中,如下图所示:

Hibernate entity state transitions

合并独立实体

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

考虑到我们保留了以下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会将分离的实体的状态复制到当前管理的实体上,并且在Persistence Context flush期间,如果dirty checking mechanism发现该实体的状态将生成UPDATE。受管实体已更改。

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

重新连接独立实体

休眠,但JPA不支持通过update方法进行重新连接。

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

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

考虑到我们保留了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进入休眠Session

merge不同,所提供的分离实体将与当前的持久性上下文重新关联,并且在刷新期间调度UPDATE,无论该实体是否已修改。

为防止这种情况,可以使用@SelectBeforeUpdate休眠注释,该注释将触发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将抛出一个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方法,因为它可以防止丢失更新。有关此主题的更多详细信息,请查看this article

update对批量更新很有用,因为它可以防止由merge操作生成的附加SELECT语句,从而减少了批量更新的执行时间。

答案 4 :(得分:12)

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

session.lock(entity, LockMode.NONE);

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

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

使用get也可以工作,除非你获得映射的继承(它已经在getId()上引发了异常)。

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

答案 5 :(得分:9)

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

答案 6 :(得分: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;
}

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

答案 8 :(得分: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);
}

答案 9 :(得分:2)

抱歉,似乎无法添加评论(但是?)。

使用Hibernate 3.5.0-Final

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

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

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

初步测试表明它可以治疗。

答案 10 :(得分:2)

也许它在Eclipselink上的表现略有不同。要重新附加分离的对象而不获取过时的数据,我通常会这样做:

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

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

em.refresh(obj)

答案 11 :(得分:1)

在原始帖子中,提到了两种方法,update(obj)merge(obj),但在相反的情况下。如果确实如此,那么为什么不先测试对象是否已经在会话中,然后再调用update(obj),否则调用merge(obj)

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

if (session.contains(obj))
{
    session.update(obj);
}
else 
{
    session.merge(obj);
}

答案 12 :(得分:1)

要重新挂接此对象,必须使用merge();

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

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

答案 13 :(得分:1)

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

答案 14 :(得分:0)

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

答案 15 :(得分:0)

Hibernate支持通过服务方式重新连接分离的实体,请参见[https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#pc-detach-reattach]

答案 16 :(得分:0)

属性hibernate.allow_refresh_detached_entity为我解决了问题。但这是一个一般规则,因此如果您仅想在某些情况下执行此操作就不太合适。希望对您有所帮助。

在Hibernate 5.4.9上测试

SessionFactoryOptionsBuilder

答案 17 :(得分:-6)

try getHibernateTemplate().saveOrUpdate()