实体状态
JPA定义了以下实体状态:
新建(瞬态)
一个新创建的对象,尚未与Hibernate的Session
(也称为Persistence Context
)关联,并且未映射到任何数据库表行,被视为处于新建(瞬态)状态。
要持久化此对象,我们需要显式调用EntityManager#persist
方法或使用传递性持久化机制。
持久化(托管)
持久化实体已与数据库表行关联,并由当前运行的Persistence Context管理。对这样的实体所做的任何更改都将被检测并传播到数据库(在Session刷新期间)。
通过Hibernate,我们不再需要执行INSERT/UPDATE/DELETE语句。Hibernate采用事务性写后工作方式,在最后负责的时刻,在当前Session
刷新期间同步更改。
游离
一旦当前运行的Persistence Context关闭,所有先前受管理的实体都会变成游离状态。后续更改将不再被跟踪,并且不会发生自动数据库同步。
实体状态转换
可以使用EntityManager
接口定义的各种方法来更改实体状态。
为了更好地理解JPA实体状态转换,请考虑以下图表:
![JPA实体状态转换](https://istack.dev59.com/Hez6p.webp)
在使用JPA时,要将游离实体重新关联到活动的EntityManager
中,可以使用merge操作。
在使用本机Hibernate API时,除了merge
之外,还可以使用更新方法将游离实体重新附加到活动的Hibernate Session中,如下图所示:
![Hibernate实体状态转换](https://istack.dev59.com/CpfAe.webp)
合并游离实体
合并操作将把游离实体状态(源)复制到托管实体实例(目标)中。
假设我们已经持久化了以下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
UPDATE
book
SET
author = 'Vlad Mihalcea',
isbn = '978-9730228236',
title = 'High-Performance Java Persistence, 2nd edition'
WHERE
id = 1
如果合并的实体在当前EntityManager中没有同等实体,则会从数据库获取新的实体快照。一旦有了托管实体,JPA将把分离的实体状态复制到当前托管的实体上,并在持久化上下文刷新期间,如果脏检查机制发现托管实体已更改,则生成更新。因此,在使用合并时,合并操作后分离的对象实例仍将保持分离状态。
重新附加分离的实体
Hibernate但不支持JPA通过update方法重新附加。Hibernate Session只能为给定数据库行关联一个实体对象。这是因为持久化上下文充当内存缓存(第一级缓存),并且只有一个值(实体)与给定键(实体类型和数据库标识符)相关联。只有在当前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语句:
UPDATE
book
SET
author = 'Vlad Mihalcea',
isbn = '978-9730228236',
title = 'High-Performance Java Persistence, 2nd edition'
WHERE
id = 1
update
方法需要你将 EntityManager
转换成 Hibernate 的 Session
。
与 merge
不同的是,提供的分离实体将重新关联到当前持久化上下文并且会在 flush 期间安排 UPDATE,无论实体是否已修改。
为了防止这种情况发生,你可以使用 @SelectBeforeUpdate
Hibernate 注解,它将触发一个 SELECT 语句来获取已加载状态,然后由脏检查机制使用。
@Entity(name = "Book")
@Table(name = "book")
@SelectBeforeUpdate
public class Book {
}
注意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 已经包含一个与我们传递给 update 的 Book 实体具有相同标识符的实体,而持久性上下文无法容纳两个表示同一实体的实例。
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语句,从而减少批量更新的执行时间。
refresh()
方法?在查看2.0规范时,我没有看到任何理由;只是说明它不被允许。 - FGreglock(LockMode.NONE)
,它确实会将实体重新附加到会话中。请参见https://dev59.com/oXNA5IYBdhLWcg3wjOve#3683370。 - seanf