图源:
本文将介绍 Hibernate 的 Session 接口,以及如何用 Session 的相关 API 转换实体(Entity)的生命周期状态。
如果缺少的 JPA 和 Hibernate 的基本认识,可以阅读。
概念
持久化上下文
在 JPA 的相关概念中,存在一个持久化上下文(Persistence Context)。
持久化上下文处于代码端与数据库之间,充当一个容器或一级缓存的作用,负责管理运行时的实体(Entity),它可以在合适的时间从数据库中加载数据到实体对象,也可以将实体对象“回写”到数据库。
在 Hibernate 中,持久化上下文由 org.hibernate.Session
实例表示,在标准的 JPA 中,表现为jakarta. persistence. EntityManager
。在使用 Hibernate 的时候,这两者都可以使用,但相比EntityManager
,Session
是一个更丰富的接口,有时候可能会更有用。
实体状态
与持久化上下文(本文特指Session
)关联的实体实例是存在状态的,它们必然处于以下三种状态之一:
-
transient,此实例从来没有附加到 Session,且数据库中也不存在对应的行数据,这只是一个为保存数据到数据库创建的新对象。
-
persistent,实例与唯一的 Session 对象关联,并对应数据库中的一条记录。Session 刷新后,将检查数据一致性,并在不一致的情况下更新数据库中的数据。
-
detached,实例曾经与 Session 关联(处于 persistent 状态),但当前已经不是。将实例从 Session 逐出(
Session.evict
)、关闭 Session 或将实例序列化/反序列化都会让实例进入这个状态。
可以通过 Session 的 API 将实例的状态进行转换:
图源:
图中的一些方法调用已经作废,比如
save()
。
持久的实体是最为关键的,处于这种状态的实体实例会被 Session 管理和监控,对这些实体的任何改变都会被记录,且在事务提交或 Session 关闭时回写到数据库,且不需要我们调用任何其它方法。
持久实体也被称作“被管理的实体”(Managed Entity)。实际上这些实体都有一个唯一的数据库标识(database identifier),所以对这些实体的任何更改都会被传播到数据库。
这也是为什么在实体类中要用
@Id
定义一个主键字段。要牢记,只有在事务提交后才会真正向数据库中插入数据,但在此之前,也会为持久实体分配数据库标识。
Session API
下面我们看相关的 Session API。
在介绍相关 API 之前,需要对一些用到的工具类进行简要说明。
public class HibernateLifecycleUtil {
public List<EntityEntry> getManagedEntities(Session session) {
// 传入的参数 session 不能是一个代理对象,否则会报类型转换错误
Map.Entry<Object, EntityEntry>[] entries = ((SessionImplementor) session).getPersistenceContext().reentrantSafeEntityEntries();
return Arrays.stream(entries).map(e -> e.getValue()).collect(Collectors.toList());
}
}
HibernateLifecycleUtil.getManagedEntities
方法可以返回 Session 管理的实体实例(persistent)。
public class DirtyDataRecorderInterceptor implements Interceptor, Serializable {
private static final List<Object> dirtyEntities = Collections.synchronizedList(new ArrayList<>());
public boolean onFlushDirty(Object entity, Object id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) throws CallbackException {
boolean b = Interceptor.super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
dirtyEntities.add(entity);
return b;
}
public static List<Object> getDirtyEntities() {
return dirtyEntities;
}
public static void clearDirtyEntitites() {
dirtyEntities.clear();
}
}
DirtyDataRecorderInterceptor
是一个 Hiberante 的拦截器,在这里用于记录变成“脏数据”的实体实例(被修改过的 persistent 实体实例)。
之前的 Hibernate 拦截器往往通过扩展
EmptyInterceptor
实现,该类已经作废。
persist
使用persist
方法可以将一个瞬时(transient)的实体实例变成持久的(关联到 Session):
Session session = sessionFactory.withOptions().interceptor(new DirtyDataRecorderInterceptor()).openSession();
Transaction transaction = session.beginTransaction();
var student = new Student("icexmoon", LocalDate.of(1990, 1, 1), Gender.MALE);
session.persist(student);
transaction.commit();
session.close();
事务提交后就能看到数据库中多了一条新纪录。
这里只展示测试用例中的关键代码,可以从这里查看。
特别的,如果实体实例已经是持久的,那么调用persist
方法不会有任何影响。如果实体实例是分离的,则会产生一个异常:
// 添加实体,实体变成持久化的
session.persist(student);
// 删除实体,实体变成已分离的
session.evict(student);
// 尝试添加已分离的实体,会抛出一个PersistentObjectException异常
Assertions.assertThrows(PersistentObjectException.class, () -> {
session.persist(student);
});
merge
使用merge
方法,可以用一个实体实例“更新” Session 中的对应实体实例。
一个典型的用法是在实体实例被分离后,改变其数据,并合并(merge)回 Session:
List<Student> students = session.createQuery("from user_student", Student.class)
.getResultList();
var icexmoon = students.stream().filter(student -> student.getName().equals("icexmoon"))
.findFirst().get();
// 分离实体,分离后可以对实体进行序列化/反序列化等操作
session.evict(icexmoon);
icexmoon.setBirthDay(LocalDate.of(2000, 5, 1));
Student mergedIcexmoon = session.merge(icexmoon);
// 合并后的 entity 与原始 entity 不是同一个对象,但内容一致
Assertions.assertNotSame(mergedIcexmoon, icexmoon);
Assertions.assertEquals(mergedIcexmoon, icexmoon);
要注意的是,merge
方法被调用后会返回“被管理的实体实例”,且该实例与用于合并的实例并不是同一个对象。前者是持久的,后者依然是分离的。
如果实体实例是瞬时的,调用merge
方法会新建一个持久的实体实例并返回:
// 用一个新的 Entity 添加
Student lalala = new Student("lalala", LocalDate.of(2001, 1, 1), Gender.MALE);
Student mergedLalala = session.merge(lalala);
// 合并后的 entity 与原始 entity 不是同一个对象,但内容一致
Assertions.assertNotSame(mergedLalala, lalala);
Assertions.assertEquals(mergedLalala, lalala);
如果实体实例已经是持久的,调用这个方法不会有任何影响。
evict & remove
前边已经多次演示了evict
的用途——将持久实体从 Session 中“驱逐”(变成分离实体)。
要注意的是,evict
仅改变了实体的状态,并不会影响数据库(不会将对应数据删除):
List<Student> students = session.createQuery("from user_student", Student.class)
.getResultList();
var icexmoon = students.stream().filter(s -> s.getName().equals("icexmoon")).findFirst().get();
session.evict(icexmoon);
transaction.commit();
session.close();
var modifiedStudents = studentRepository.findAll();
Assertions.assertEquals(this.students.size() , modifiedStudents.size());
remove
会将持久实体从 Session 中移除,变成瞬时实体。换言之,会删除数据库中对应的数据:
List<Student> students = session.createQuery("from user_student", Student.class)
.getResultList();
var icexmoon = students.stream().filter(s -> s.getName().equals("icexmoon")).findFirst().get();
session.remove(icexmoon);
transaction.commit();
session.close();
var modifiedStudents = studentRepository.findAll();
Assertions.assertEquals(this.students.size() - 1, modifiedStudents.size());
因为remove
后的实体状态是瞬时的(transient),所以可以用persist
再次添加:
var icexmoon = students.stream().filter(s -> s.getName().equals("icexmoon")).findFirst().get();
session.remove(icexmoon);
session.persist(icexmoon);
身份域
通常我们并不会直接修改(或添加)实体对象中的身份域(Indentity field),身份域都是由 Session 赋予和管理的。但如果我们愿意,完全可以通过修改和指定身份域去更新指定的持久实体:
var icexmoon = students.stream().filter(s -> s.getName().equals("icexmoon")).findFirst().get();
var student = new Student("icexmoon", LocalDate.of(2002, 1, 1), Gender.MALE);
student.setId(icexmoon.getId());
session.merge(student);
在这个示例中,并没有像常见的那样直接修改持久实体icexmoon
中的属性来变更数据,而是创建了一个新的瞬时实体student
,并且通过setId
方法为其指定了和持久实体相同的身份域,此时调用merge
方法,就不再是添加一个新的持久实体,而是修改了已有的持久实体(因为它们的 id 相同),并最终修改了数据库中的数据。
一般而言实体类不需要 id 的 Setter,这里仅为了实现测试用例而添加。
The End,谢谢阅读。
可以从
文章评论