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

Ste*_*all 179 java session spring hibernate detach

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

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

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

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

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

小智 176

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

merge() 将陈旧状态推送到DB,并覆盖任何干预更新.

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

lock() 不能在一个分离的实体上调用,即使它可以,它确实重新附加了实体,使用参数'LockMode.NONE'调用'lock'意味着你锁定但不锁定,是API设计中最违反直觉的一部分我见过的.

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

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

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

MIK

  • 这绝对不准确.从JPwH:`*重新附加修改的分离实例*通过调用分离对象上的update(),可以将分离的实例重新附加到新的Session(并由此新的持久化上下文管理).根据我们的经验,如果您在脑海中重命名update()方法以重新附加(),则可能更容易理解以下代码 - 但是,有一个很好的理由称为更新.更多内容可以在9.3节中找到0.2 (11认同)
  • 根据 Hibernate javadoc(但不是 JPA),`lock(LockMode.NONE)` 实际上可以在瞬态对象上调用,并且它确实将实体重新附加到会话。见 http://stackoverflow.com/a/3683370/14379 (3认同)

Ste*_*ole 28

所有这些答案都错过了一个重要的区别.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 );
}
Run Code Online (Sandbox Code Playgroud)

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

  • 这是这个问题的正确答案 - 案件结束了! (2认同)
  • `Session.contains(Object)`通过引用检查.如果已有另一个实体在会话中表示相同的行,并且您传递了一个分离的实例,则会出现异常. (2认同)

Vla*_*cea 22

实体状态

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;
});
Run Code Online (Sandbox Code Playgroud)

当实体处于分离状态时,我们对其进行如下修改:

_book.setTitle(
    "High-Performance Java Persistence, 2nd edition"
);
Run Code Online (Sandbox Code Playgroud)

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

doInJPA(entityManager -> {
    Book book = entityManager.merge(_book);
 
    LOGGER.info("Merging the Book entity");
 
    assertFalse(book == _book);
});
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

重新附加一个分离的实体

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

一个 HibernateSession只能为给定的数据库行关联一个实体对象。这是因为 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"
);
Run Code Online (Sandbox Code Playgroud)

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

doInJPA(entityManager -> {
    Session session = entityManager.unwrap(Session.class);
 
    session.update(_book);
 
    LOGGER.info("Updating the Book entity");
});
Run Code Online (Sandbox Code Playgroud)

而 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
Run Code Online (Sandbox Code Playgroud)

update方法需要你unwrapEntityManager一个休眠Session

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

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

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

小心 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
    );
}
Run Code Online (Sandbox Code Playgroud)

现在,当执行上面的测试用例时,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)
Run Code Online (Sandbox Code Playgroud)

结论

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

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


cwa*_*ash 19

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

外交答案:这在Hibernate文档中有所描述.如果您需要更多说明,请查看Java Persistence with Hibernate的第9.3.2节"使用分离对象".如果你做的不仅仅是使用Hibernate的CRUD,我强烈建议你拿到这本书.

  • 来自http://www.seamframework.org/:“ Red Hat已停止了Seam 3的积极开发。”链接“此Seam的文档”也已失效。 (4认同)

Joh*_*zzo 12

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

session.lock(entity, LockMode.NONE);
Run Code Online (Sandbox Code Playgroud)

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

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

使用get也可以工作,除非你获得映射的继承(这将在getId()上抛出异常).

entity = session.get(entity.getClass(), entity.getId());
Run Code Online (Sandbox Code Playgroud)

  • 我想将一个实体与会话重新关联.不幸的是,`Session.lock(entity,LockMode.NONE)`失败,但有例外说:无法重新关联未初始化的瞬态集合.怎么能克服这个? (2认同)

小智 9

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

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

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

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

saveOrUpdate():保存或更新

lock():已弃用

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

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

因此,lock()不应该直接使用并且基于功能要求可以选择它们中的一个或多个.


Ver*_*mid 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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在每个对象上调用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;
}
Run Code Online (Sandbox Code Playgroud)


Ben*_*ond -6

try getHibernateTemplate().saveOrUpdate()
Run Code Online (Sandbox Code Playgroud)