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


186

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

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

  1. getHibernateTemplate().update( obj ) 当且仅当休眠会话中不存在对象时,此方法才有效。当以后需要时,将引发异常,指出具有给定标识符的对象已在会话中存在。

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

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

Answers:


181

因此,似乎没有办法在JPA中重新附加陈旧的独立实体。

merge() 会将过时的状态推送到数据库,并覆盖所有中间更新。

refresh() 不能在独立实体上调用。

lock() 无法在分离的实体上调用,即使可以,也确实重新附加了该实体,并使用参数LockMode.NONE调用“ lock”,这意味着您正在锁定而不是锁定是API设计中最违反直觉的部分我见过的。

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

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

看来唯一的方法是丢弃陈旧的分离实体,并执行具有相同ID的查找查询,这将影响L2或DB。

米克


1
我想知道是否有JPA规范不允许refresh()在分离的实体上使用的原因?查看2.0规范,我看不出任何理由。只是不允许这样做。
FGreg

11
这绝对是不准确的。来自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节
-cwash

持久对象工作得很好,脏标志是根据初始负载和flush()时的值之间的差值设置的。分离的对象需要并且当前不具有此功能。休眠的方法是为分离的对象添加额外的哈希/ ID。并保留可用的分离对象的最后状态快照,就像它们对持久对象所做的一样。因此,他们可以利用所有现有代码,并使它适用于分离的对象。就像@mikhailfranco指出的那样,我们不会“将过时的状态推送到数据库,并覆盖所有中间的更新”
tom 2014年

2
根据Hibernate javadoc(但不是JPA),lock(LockMode.NONE)实际上可以在一个临时对象上调用它,并且它确实将实体重新附加到会话上。参见stackoverflow.com/a/3683370/14379
seanf

锁对我不起作用:java.lang.IllegalArgumentException:实体不在org.hibernate.internal.SessionImpl.lock(SessionImpl.java:3491)的持久化上下文中,而在org.hibernate.internal.SessionImpl.lock(SessionImpl。 java:3482),网址为com.github.vok.framework.DisableTransactionControlEMDelegate.lock(DB.kt)
Martin Vysny

32

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

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()。


3
这是此问题的正确答案-结案了!
cwash

2
Session.contains(Object)通过参考检查。如果已经有另一个Entity代表会话中的同一行,并且您传递了一个分离的实例,则将获得异常。
djmj 2014年

作为Session.contains(Object)参考检查,如果会话中存在另一个表示同一行的Entity,它将返回false,并将对其进行更新。
AxelWass

19

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

外交回答:这在Hibernate docs中有描述。如果需要更多说明,请参阅Java Persistence with Hibernate的 9.3.2节,即“使用分离的对象”。如果您使用Hibernate进行CRUD以外的工作,我强烈建议您阅读这本书。


5
来自seamframework.org:“ Seam 3的积极开发已被Red Hat停止。”“ Seam的这部分文档”链接也已失效。
badbishop

14

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

session.lock(entity, LockMode.NONE);

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

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

使用get也可能有效,除非当您继承映射时(继承已在getId()上引发异常)。

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

2
我想将一个实体与会话重新关联。不幸的是,Session.lock(entity, LockMode.NONE)失败并显示以下异常:无法重新关联未初始化的瞬时集合。如何克服呢?
dma_k 2010年

1
实际上我并不完全正确。使用lock()可重新附加您的实体,但不附加与它绑定的其他实体。因此,如果您执行entity.getOtherEntity()。getYetAnotherEntity(),则可能会有LazyInit异常。我知道要克服的唯一方法是使用查找。实体= em.find(entity.getClass(),实体.getId();
约翰·里佐,

没有Session.find()API方法。也许你是说Session.load(Object object, Serializable id)
dma_k 2011年

11

由于这是一个非常常见的问题,因此我写了 这篇文章,此答案基于该文章

实体状态

JPA定义以下实体状态:

新(瞬态)

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

要坚持下去,我们需要明确地调用 EntityManager#persist方法或使用可传递持久性机制。

持久(托管)

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

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

独立式

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

实体状态转换

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

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

JPA实体状态转换

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

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

休眠实体状态转换

合并独立实体

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

考虑到我们已经持久化了以下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 。

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

重新附加独立实体

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

冬眠 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方法需要你unwrapEntityManager一个休眠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将抛出a,NonUniqueObjectException因为第二个EntityManager已经包含Book与我们传递给它的标识符具有相同标识符的实体update,并且Persistence Context无法容纳同一实体的两个表示形式。

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,因为它可以防止由产生的额外的SELECT语句有利于批量更新merge操作,从而降低了批量更新的执行时间。


好答案。我想知道@SelectBeforeUpdate注释。选择何时触发?在调用时update,就在刷新之前还是没有关系(休眠是否在刷新之前一次调用中获取了所有带注释的实体就可能很重要)?
Andronicus

@SelectBeforeUpdate在持久性上下文flush操作期间,触发器将触发SELECT 。请查看中getDatabaseSnapshot方法以DefaultFlushEntityEventListener获取更多详细信息。
Vlad Mihalcea

10

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

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

因此update()saveOrUpdate()lock()replicate()并且merge()是候选人的选择。

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

saveOrUpdate():保存或更新

lock():已弃用

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

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

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


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

因为Contains始终为false,所以在每个对象上调用First Lock。问题在于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;
}

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


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

2

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

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

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


2

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

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

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

em.refresh(obj)


1

在原来的职位,有两种方法,update(obj)并且merge(obj)被提到的工作,但在相反的情况。如果确实如此,那么为什么不先测试以查看对象是否已经在会话中,然后调用(update(obj)如果存在),否则调用merge(obj)

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

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

2
contains()检查通过引用进行比较,但是休眠函数通过数据库ID进行工作。session.merge将永远不会在您的代码中被调用。
Verena Haunschmid

1

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

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

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

0

在某些用例中,先调用merge()(以更新持久实例),然后再调用lock(LockMode.NONE)(以附加当前实例,而不是由merge()返回的实例)。


0

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

在Hibernate 5.4.9上测试

SessionFactoryOptionsBuilder



By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.