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

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

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

    <李> < p > getHibernateTemplate().update( obj ) 当且仅当对象在hibernate会话中还不存在时,这才有效。当我稍后需要它时,抛出异常,说明具有给定标识符的对象已经存在于会话中 <李> < p > getHibernateTemplate().merge( obj ) 当且仅当hibernate会话中存在对象时,此操作才有效。如果我使用这个,当我需要对象稍后处于会话中时,就会抛出异常

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

201916 次浏览
try getHibernateTemplate().saveOrUpdate()

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

你可能正在寻找一个扩展的持久化上下文。这是Seam框架…如果你特别纠结于在Spring中使用Hibernate,可以查看Seam文档中的这一块

外交回答:Hibernate文档中描述。如果你需要更多的说明,请查看Java持久化与Hibernate的9.3.2节,称为“使用分离对象”。如果你用Hibernate做CRUD以外的事情,我强烈推荐你读这本书。

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

session.lock(entity, LockMode.NONE);

它不会锁定任何东西,但它会从会话缓存中获取实体,或者(如果在那里没有找到)从DB中读取它。

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

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

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

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

merge()将把stale状态推到DB,

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

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

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

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

.管理代码 似乎唯一的方法就是抛弃你陈旧的分离实体 并使用相同的id执行find查询,这将命中L2或DB.

麦克指标

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

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

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

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

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

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

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

我这样做在c#与NHibernate,但它应该在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;
}

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

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

使用Hibernate 3.5.0-Final

虽然Session#lock方法已弃用,但javadoc 建议使用__abc1,如果你确保你的关联有cascade=lock,惰性加载也不是问题。

我的attach方法看起来有点像

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

初步测试表明它很有效。

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

也许它在Eclipselink上的表现略有不同。为了重新连接分离的对象而不获得陈旧的数据,我通常这样做:

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

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

em.refresh(obj)

要重新连接该对象,必须使用merge();

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

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

属性hibernate.allow_refresh_detached_entity为我做到了这一点。但这是一个普遍的规则,所以如果你只想在某些情况下这样做,那就不太合适了。我希望这能有所帮助。

在Hibernate 5.4.9上测试

SessionFactoryOptionsBuilder

实体状态

JPA定义了以下实体状态:

新(瞬态)

如果一个新创建的对象从未与Hibernate Session (a.k.a 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

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

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

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

重新连接一个分离的实体

Hibernate,但不支持JPA通过update方法重新连接。

Hibernate Session只能为给定的数据库行关联一个实体对象。这是因为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"
);

我们可以像这样重新连接分离的实体:

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方法要求你将EntityManager unwrap为Hibernate Session

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

为了防止这种情况,你可以使用@SelectBeforeUpdate Hibernate注释,它将触发一个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已经包含了一个__abc2实体,其标识符与我们传递给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语句,因此减少批量更新的执行时间。