红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

从零开始 Spring Boot 49:Hibernate Entity Lifecycle

2023年6月28日 1221点热度 0人点赞 0条评论

spring boot

图源:简书 (jianshu.com)

本文将介绍 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 将实例的状态进行转换:

image-20230628104714585

图源:Baeldung

图中的一些方法调用已经作废,比如save()。

持久的实体是最为关键的,处于这种状态的实体实例会被 Session 管理和监控,对这些实体的任何改变都会被记录,且在事务提交或 Session 关闭时回写到数据库,且不需要我们调用任何其它方法。

持久实体也被称作“被管理的实体”(Managed Entity)。实际上这些实体都有一个唯一的数据库标识(database identifier),所以对这些实体的任何更改都会被传播到数据库。

  • 这也是为什么在实体类中要用@Id定义一个主键字段。

  • 要牢记,只有在事务提交后才会真正向数据库中插入数据,但在此之前,也会为持久实体分配数据库标识。

Session API

下面我们看相关的 Session API。

在介绍相关 API 之前,需要对一些用到的工具类进行简要说明。

@Component
public class HibernateLifecycleUtil {
    @SneakyThrows
    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<>());
​
    @Override
    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,谢谢阅读。

可以从这里获取本文的完整示例代码。

参考资料

  • Hibernate Entity Lifecycle | Baeldung

  • Hibernate: save,persist, update, merge | Baeldung

  • 从零开始 Spring Boot 48:JPA & Hibernate - 红茶的个人站点 (icexmoon.cn)

  • What Is the JDK com.sun.proxy.$Proxy Class? | Baeldung

  • Dynamic Proxies in Java | Baeldung

  • spring boot 中如何设置hibernate Interceptor

  • Hibernate Interceptors | Baeldung

  • Guide to the Hibernate EntityManager | Baeldung

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: entity hibernate lifecycle session spring
最后更新:2023年6月28日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号