我有一种情况,我需要将分离的对象重新附加到休眠会话,尽管会话中可能已经存在相同标识的对象,这会导致错误。
现在,我可以做两件事之一。
getHibernateTemplate().update( obj ) 当且仅当对象在休眠会话中不存在时才有效。当我以后需要它时,会抛出异常,说明会话中已经存在具有给定标识符的对象。 getHibernateTemplate().merge( obj ) 当且仅当休眠会话中存在对象时,此方法才有效。如果我使用它,稍后我需要对象在会话中时会引发异常。
鉴于这两种情况,我一般如何将会话附加到对象?我不想使用异常来控制这个问题的解决方案的流程,因为必须有一个更优雅的解决方案......
因此,似乎没有办法在 JPA 中重新附加过时的分离实体。
merge()
会将陈旧状态推送到数据库,并覆盖任何干预更新。
refresh()
不能在分离的实体上调用。
lock()
不能在分离的实体上调用,即使它可以,并且它确实重新连接了实体,使用参数 'LockMode.NONE' 调用'lock' 暗示你正在锁定,但不是锁定,是最违反直觉的部分我见过的 API 设计。
所以你被卡住了。有一个 detach()
方法,但没有 attach()
或 reattach()
。您无法使用对象生命周期中的明显步骤。
从关于 JPA 的类似问题的数量来看,似乎即使 JPA 确实声称有一个连贯的模型,它也肯定与大多数程序员的心智模型不匹配,他们被诅咒浪费很多时间试图理解如何获得JPA 做最简单的事情,并最终在其应用程序中使用缓存管理代码。
似乎唯一的方法是丢弃陈旧的分离实体并使用相同的 id 进行查找查询,这将命中 L2 或 DB。
米克
所有这些答案都忽略了一个重要的区别。 update() 用于(重新)将您的对象图附加到会话。您传递给它的对象是被管理的对象。
merge() 实际上不是一个(重新)附件 API。注意 merge() 有返回值吗?那是因为它会返回托管图,这可能不是您传递给它的图。 merge() 是一个 JPA API,其行为受 JPA 规范的约束。如果您传递给 merge() 的对象已经被管理(已经与会话相关联),那么这就是 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() ...
Session.contains(Object)
通过引用进行检查。如果会话中已经有另一个实体表示同一行并且您传递了一个分离的实例,您将得到一个异常。
Session.contains(Object)
通过引用检查时,如果有 另一个 实体表示会话中的同一行,它将返回 false,并对其进行更新。
实体状态
JPA 定义了以下实体状态:
新(瞬态)
从未与 Hibernate Session
(又名 Persistence Context
)关联且未映射到任何数据库表行的新创建对象被视为处于新建(瞬态)状态。
要持久化,我们需要显式调用 EntityManager#persist
方法或使用传递持久性机制。
持久(托管)
持久性实体已与数据库表行相关联,并由当前运行的持久性上下文管理。对此类实体所做的任何更改都将被检测到并传播到数据库(在会话刷新期间)。
使用 Hibernate,我们不再需要执行 INSERT/UPDATE/DELETE 语句。 Hibernate 采用事务性后写工作方式,并且在当前 Session
刷新时间期间的最后一个负责时刻同步更改。
分离式
一旦当前运行的持久性上下文关闭,所有以前管理的实体都将被分离。将不再跟踪连续的更改,也不会发生自动数据库同步。
实体状态转换
您可以使用 EntityManager
接口定义的各种方法更改实体状态。
为了更好地理解 JPA 实体状态转换,请考虑下图:
https://i.stack.imgur.com/Hez6p.png
使用 JPA 时,要将分离的实体重新关联到活动的 EntityManager
,您可以使用 merge 操作。
使用原生 Hibernate API 时,除了 merge
,您可以使用更新方法将分离的实体重新附加到活动的 Hibernate 会话,如下图所示:
https://i.stack.imgur.com/CpfAe.png
合并分离的实体
合并会将分离的实体状态(源)复制到托管实体实例(目标)。
假设我们已经持久化了以下 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。
因此,当使用合并时,即使在合并操作之后,分离的对象实例仍将继续保持分离状态。
重新附加分离的实体
Hibernate,但 JPA 不支持通过 update
方法重新附加。
Hibernate 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 方法要求您将 EntityManager 解包到 Hibernate Session。
与 merge
不同,提供的分离实体将与当前持久性上下文重新关联,并且无论实体是否已修改,都会在刷新期间安排更新。
为了防止这种情况,您可以使用 @SelectBeforeUpdate
Hibernate 注释,该注释将触发一个 SELECT 语句,该语句获取加载状态,然后由脏检查机制使用。
@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {
//Code omitted for brevity
}
当心 NonUniqueObjectException
update
可能出现的一个问题是,如果 Persistence Context 已包含具有相同 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
方法,因为它可以防止丢失更新。
update
适合批量更新,因为它可以防止 merge
操作生成额外的 SELECT 语句,从而减少批量更新执行时间。
@SelectBeforeUpdate
注释。什么时候触发选择?在刷新之前调用 update
还是无关紧要(如果 hibernate 在刷新之前在一次调用中获取所有带注释的实体可能很重要)?
@SelectBeforeUpdate
在持久性上下文 flush
操作期间触发 SELECT。查看 the getDatabaseSnapshot
method in the DefaultFlushEntityEventListener
了解更多详情。
平淡无奇的回答:您可能正在寻找扩展的持久性上下文。这是 Seam Framework 背后的主要原因之一...如果您特别难以在 Spring 中使用 Hibernate,请查看 Seam 文档的 this piece。
外交回答:这在Hibernate docs中有所描述。如果您需要更多说明,请查看 Java Persistence with Hibernate 的第 9.3.2 节,称为“使用分离的对象”。如果您使用 Hibernate 做的不仅仅是 CRUD,我会强烈建议您阅读这本书。
如果您确定您的实体没有被修改(或者如果您同意任何修改都将丢失),那么您可以将其重新附加到带锁的会话中。
session.lock(entity, LockMode.NONE);
它不会锁定任何内容,但它会从会话缓存中获取实体,或者(如果没有找到)从数据库中读取它。
当您从“旧”(例如来自 HttpSession)实体导航关系时,防止 LazyInitException 非常有用。您首先“重新附加”实体。
使用 get 也可以工作,除非你得到继承映射(这已经在 getId() 上抛出异常)。
entity = session.get(entity.getClass(), entity.getId());
Session.lock(entity, LockMode.NONE)
失败并出现异常提示:无法重新关联未初始化的瞬态集合。如何克服这一点?
Session.find()
API 方法。也许您的意思是 Session.load(Object object, Serializable id)
。
我回到 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()
不应直接使用,并且可以根据功能要求选择其中的一个或多个。
我用 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;
}
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 的少数几个方面之一,关于更改的实体及其属性的不同附加选项。
我想出了一个解决方案来“刷新”持久存储中的一个对象,该对象将解释可能已经附加到会话的其他对象:
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);
}
抱歉,似乎无法添加评论(还没有?)。
使用 Hibernate 3.5.0-Final
虽然不推荐使用 Session#lock
方法,但 javadoc 确实 建议使用 Session#buildLockRequest(LockOptions)#lock(entity)
,如果您确保您的关联具有 cascade=lock
,则延迟加载也不是问题。
所以,我的附加方法看起来有点像
MyEntity attach(MyEntity entity) {
if(getSession().contains(entity)) return entity;
getSession().buildLockRequest(LockOptions.NONE).lock(entity);
return entity;
初步测试表明它是一种享受。
也许它在 Eclipselink 上的行为略有不同。为了在不获取陈旧数据的情况下重新附加分离的对象,我通常会这样做:
Object obj = em.find(obj.getClass(), id);
作为可选的第二步(使缓存失效):
em.refresh(obj)
尝试 getHibernateTemplate().replicate(entity,ReplicationMode.LATEST_VERSION)
在原始帖子中,提到了两种方法 update(obj)
和 merge(obj)
可以工作,但在相反的情况下。如果真是这样,那为什么不先测试一下对象是否已经在会话中,如果是则调用update(obj)
,否则调用merge(obj)
。
会话中存在的测试是 session.contains(obj)
。因此,我认为以下伪代码会起作用:
if (session.contains(obj))
{
session.update(obj);
}
else
{
session.merge(obj);
}
要重新附加此对象,您必须使用 merge();
此方法在参数中接受您的实体分离并返回一个实体将被附加并从数据库重新加载。
Example :
Lot objAttach = em.merge(oldObjDetached);
objAttach.setEtat(...);
em.persist(objAttach);
调用第一个 merge() (更新持久实例),然后调用 lock(LockMode.NONE) (附加当前实例,而不是 merge() 返回的实例)似乎适用于某些用例。
属性 hibernate.allow_refresh_detached_entity
对我有用。但这是一个通用规则,所以如果你只想在某些情况下这样做,它不是很合适。我希望它有所帮助。
在休眠 5.4.9 上测试
try getHibernateTemplate().saveOrUpdate()
refresh()
?纵观 2.0 规范,我看不出任何理由;只是它是不允许的。*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 节中找到lock(LockMode.NONE)
实际上可以在瞬态对象上调用,并且它确实将实体重新附加到会话。请参阅stackoverflow.com/a/3683370/14379