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

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

getHibernateTemplate()。更新(obj) 当且仅当对象在hibernate会话中还不存在时,这才有效。当我以后需要它时,抛出异常,声明具有给定标识符的对象已经存在于会话中。 getHibernateTemplate()。合并(obj) 当且仅当hibernate会话中存在对象时,此操作才有效。如果稍后使用此方法,则在需要对象处于会话中时抛出异常。

对于这两种场景,我如何将会话附加到对象?我不想使用异常来控制这个问题解决方案的流程,因为一定有更优雅的解决方案……


当前回答

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

merge()会将过期状态推送到DB, 并覆盖任何中间更新。

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

不能在分离实体上调用Lock (), 即使它可以,而且它确实重新连接了实体, 使用参数LockMode调用lock。没有' 这意味着你在锁定,但不是锁定, 是我见过的最违反直觉的API设计。

所以你陷入了困境。 有一个detach()方法,但没有attach()或reattach()。 对象生命周期中的一个明显步骤对您来说是不可用的。

根据关于JPA的类似问题的数量判断, 似乎即使JPA声称有一个一致的模型, 它肯定不符合大多数程序员的思维模式, 谁被诅咒浪费了很多时间试图理解 如何让JPA做最简单的事情,并以缓存结束 管理代码遍布他们的应用程序。

要做到这一点,似乎唯一的方法就是抛弃你陈旧的超然实体 并使用相同的id执行find查询,这将命中L2或DB。

Mik

其他回答

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

ReplicationMode.LATEST_VERSION尝试getHibernateTemplate () .replicate(实体)

实体状态

JPA定义了以下实体状态:

新(瞬态)

如果一个新创建的对象从未与Hibernate会话(也就是持久化上下文)关联过,也没有映射到任何数据库表行,则认为该对象处于New (Transient)状态。

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

持久(管理)

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

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

分离

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

实体状态转换

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

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

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

当使用原生Hibernate API时,除了merge,你还可以使用更新方法将一个分离的实体重新附加到一个活动的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

如果合并的实体在当前EntityManager中没有对等的实体,则将从数据库中获取一个新的实体快照。

一旦有了托管实体,JPA将分离实体的状态复制到当前托管的实体上,并且在持久性上下文刷新期间,如果脏检查机制发现托管实体已更改,将生成一个UPDATE。

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

重新连接一个分离的实体

Hibernate(而不是JPA)支持通过更新方法重新连接。

Hibernate会话只能为给定的数据库行关联一个实体对象。这是因为Persistence Context充当内存中的缓存(第一级缓存),并且只有一个值(实体)与给定的键(实体类型和数据库标识符)相关联。

只有在没有与当前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

更新方法要求您将EntityManager解包装为Hibernate会话。

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

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

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

注意NonUniqueObjectException

更新时可能出现的一个问题是,如果持久性上下文已经包含了一个与下面示例中相同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)

结论

如果您正在使用乐观锁定,则合并方法是首选,因为它可以防止丢失更新。

该更新适用于批量更新,因为它可以防止合并操作生成额外的SELECT语句,从而减少批量更新的执行时间。

对不起,似乎不能添加评论(还?)。

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

初步测试表明它很有效。

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