Hibernate 中持久化对象的状态(重要)

持久化对象的状态

站在持久化的角度,Hibernate 将持久化对象分为四种状态,分别是:临时状态、持久化状态、游离状态、删除状态。 Session 的特定方法能让对象从一个状态转换到另一个状态。

如果搞不懂 Hibernate 持久化对象的状态,那么就无法真正了解 Hibernate。

下面详细分析下这几种状态:

  • 临时状态(Transient):相当于你还没来公司上班,公司没你这人
    – OID 为 null
    – session 缓存中没有该对象
    – 数据库中也没有该对象对应的记录
    – 一般刚 new 出来的对象就处于这种状态。

  • 持久化状态(Persist):相当于你来公司上班了,公司花名册上有你这个人
    – OID 不为 null
    – 对象位于 session 缓存中
    – 在数据库中有与之对应的记录,并且在 flush 时会根据对象属性变化同步更新数据库
    – 在同一个 session 实例中,数据库表中的记录只对应唯一的持久化对象,即一对一。

  • 游离对象(Detached):你请假了很久,你不知道公司把你开了还是没开
    – OID 不为 null
    – session 缓存中没有该对象
    – 数据库中可能有与之对应的记录,也可能没有,不一定。一般情况下,游离对象是由持久化对象转变过来的。比如清理了缓存,session 中没有,但是数据库表中有对应的记录,这个对象就是游离的。

  • 删除对象(Removed):你被公司 Fire 了
    – 在数据库中没有该对象 OID 对应的记录
    – session 缓存中也存在该对象

上面的四种状态非常重要,一定要弄清楚。

持久化对象转换相关 API

上述四种状态的转换图如下:
Hibernate 中持久化对象的状态(重要)_第1张图片

save()

save 方法蛮多细节。
最基本的使用如下

    @Test
    public void testSave() {
        // 刚创建,处于临时状态
        News news = new News();
        news.setTitle("C");
        news.setAuthor("c_author");
        news.setDate(new Date());
        // save() 方法使得对象从临时状态变持久化状态
        session.save(news);
    }

此时成功插入一条数据到表中。
在这里插入图片描述
那么如果在 save() 前设置了 id,有用吗?

    @Test
    public void testSave() {
        News news = new News();
        // 手动设置 OID
        news.setId(555);
        news.setTitle("D");
        news.setAuthor("d_author");
        news.setDate(new Date());
        session.save(news);
    }

Hibernate 中持久化对象的状态(重要)_第2张图片
结果是依然插入成功,但是 ID 是使用我们在 hibernate.cfg.xml 中配置的自增策略生成,因此在 save() 方法前给对象设置 ID 是无效的。
那么如果在 save 后再手动修改对象的 ID 会怎样呢?

    @Test
    public void testSave() {
        News news = new News();
        news.setTitle("E");
        news.setAuthor("E_author");
        news.setDate(new Date());
        session.save(news);
        news.setId(666);
    }

在这里插入图片描述
这样就会报错,说 id 从 6 变为 666。报错原因是 Hibernate 规定 save() 后不能再去修改对象的 ID。因为 session 中的对象就是靠 ID 和数据库表中的记录进行对应,如果该了,就找不到对应的记录了。

persist() 方法

persist() 方法如果正常使用和 save() 方法其实效果一样。

    @Test
    public void testPersist() {
        News news = new News();
        news.setTitle("F");
        news.setAuthor("F_author");
        news.setDate(new Date());
        // persist 使得对象从临时状态变为持久状态
        session.persist(news);
    }

在这里插入图片描述
也是插入成功。
那么和 save() 方法不同的是,如果在 persist 之前设置了对象的 ID,则不会进行保存操作,而是抛出一个异常。

    @Test
    public void testPersist() {
        News news = new News();
        news.setId(777);
        news.setTitle("G");
        news.setAuthor("G_author");
        news.setDate(new Date());
        session.persist(news);
    }

在这里插入图片描述
同样的,如果在 persist 之后修改了对象的 ID,也会抛出异常。

get() 方法 & load() 方法

get() 和 load() 方法在下面的用法是等价的。

    @Test
    public void testGet() {
        News news = (News) session.get(News.class,1);
        System.out.println(news);
    }

    @Test
    public void testLoad() {
        News news = (News) session.load(News.class,1);
        System.out.println(news);
    }

它们都会发送 select 语句。
在这里插入图片描述
再看下面的例子:

    @Test
    public void testGet() {
        News news = (News) session.get(News.class,1);
        //System.out.println(news);
    }

    @Test
    public void testLoad() {
        News news = (News) session.load(News.class,1);
        //System.out.println(news);
    }

此时 get 会发送 select 语句,而 load 不会发送了。
get 和 load 区别如下:

  • get 方法会立即加载对象,不管你后面用不用。而 load 方法如果后面不使用查出来的对象,则不会立即执行查询操作,而是返回一个代理对象。也就是说, get 是立即加载,而 load 是懒加载
  • 当查询一个数据库表中不存在的记录时,get 方法会返回 null。但是 load 分为两种情况。
    – 不使用 load 出来的对象的任何属性,则不会有问题。
    – 使用 load 出来的对象的属性,则会抛出异常。
    @Test
    public void testGet() {
    	// 查询不存在的记录,执行 SQL
        News news = (News) session.get(News.class,99);
        // 输出 null
        System.out.println(news);
    }
    
    @Test
    public void testLoad() {
    	// 查询不存在的记录,因为后面打印了 news 对象,调用 toString() 方法
    	// 因此会用到该对象的属性,也会发送 SQL。
        News news = (News) session.load(News.class,99);
        // 不会输出 null,而是抛出异常
        System.out.println(news);
    }
    
  • load 方法还可能会抛出懒加载异常。 LazyInitializationException。当我获取到代理对象时,还没初始化代理对象时,session 被关闭了,然后我要用查询出来的对象了,那么就会抛出 LazyInitializationException 异常。为什么会抛出异常呢,这是因为 load 返回的是代理对象,当我要查数据库用数据去填充代理对象时,连接被关闭了,那么就没法去加载了,所以可能会抛出异常。

update() 方法

虽然没有主动调用过 UPDATE 方法,但是在前面在控制台也看到过 UPDATE 语句,这是因为在 commit 时,执行了 flush() 方法,当数据库表中的记录和缓存中的对象不一致时,就会发送 UPDATE 语句。因此,在下面的这种情况,掉不掉用 update() 方法结果都是一样的。

    @Test
    public void testUpdate() {
        News news = (News) session.load(News.class,1);
        news.setAuthor("SUN");
        // session.update(news); 可写可不写
    }

在这里插入图片描述
但是,如果要去更新一个游离对象,则必须手动调用 update() 方法。

    @Test
    public void testUpdate2() {
        News news = (News) session.get(News.class,1);
        // 清空缓存,则 news 变为游离对象,缓存中没有,但是数据库表中存在
        session.clear();
        news.setAuthor("ORACLE");
        // 必须手动调用 update 方法,更新游离对象
        session.update(news);
        // update 将游离对象变为持久化对象,加载到缓存中,因此下面的 select 语句不会发送
        News news2 = (News) session.get(News.class,1);
        System.out.println(news2);
    }

在这里插入图片描述
update() 方法也有几个需要注意的地方:
一是当缓存中没有,总会发送 update 语句去更新数据库,和触发器一起工作会出问题。
二是当游离的对象在数据库中没有记录,还去更新,就会报错。
三是update语句会将游离对象转换成持久化对象,当 session 中有两个相同的 OID 的持久化对象时,就会报错,这是不允许的。

saveOrUpdate() 方法

Session 的 saveOrUpdate() 方法同时包含了 save() 与 update() 方法的功能。
Hibernate 中持久化对象的状态(重要)_第3张图片

临时对象是 id 为 null 的对象,也就是刚 new 出来的对象。

判定对象为临时对象的标准

  • Java 对象的 OID 为 null
  • 映射文件中为 设置了 unsaved-value 属性, 并且 Java 对象的 OID 取值与这个 unsaved-value 属性值匹配
    一般根据 OID 是否为 null 就可以判断,比较极端才会出现第二种情况。

saveOrUpdate() 就是如果 ID null,则插入,不为 null,则更新。但是注意,如果更新的 id 不存在时,会报错
测试下 unsaved-value。


<id name="id" type="java.lang.Integer" unsaved-value="10">
    @Test
    public void testSaveOrUpdate() {
        News news = new News();
        news.setAuthor("bbbb");
        news.setTitle("BBBB");
        news.setDate(new Date());
        // 将 OID 的值设置的和 unsaved-value 值一样
        news.setId(10);
        session.saveOrUpdate(news);
    }

此时执行,会执行插入操作。前两条 SQL 是 hibernate 生成 id 算法获取 id 的 SQL 不用去管。
在这里插入图片描述
在这里插入图片描述

delete() 方法

Session 的 delete() 方法既可以删除一个游离对象, 也可以删除一个持久化对象。 只要 OID 对应的上,就能删除!

删除游离对象(session 缓存中不存在,但数据库表中存在)

    @Test
    public void testDelete() {
        News news = new News();
        news.setId(262144);
        session.delete(news);
    }

删除持久化对象(session 缓存中有,数据库表中也有):

    @Test
    public void testDelete() {
        News news = (News) session.get(News.class, 262144);
        session.delete(news);
    }

注:如果删除一个不存在的 id,则会抛出异常。

这里的 delete 并不是直接删除,而是计划删除,真正的删除是在 flush 缓存的时候删除的。看如下代码:

    @Test
    public void testDelete() {
        News news = (News) session.get(News.class, 229376);
        session.delete(news);
        System.out.println(news);
    }

打印日志中并没有执行 delete 语句。
在这里插入图片描述
我们打印该对象,发现该对象的 id 还是存在的,那么我们甚至可以拿到对象的 id 干坏事,比如删了这个对象,然后再更新这个对象,或者删了这个对象,再保存这个对象。

    @Test
    public void testDelete() {
        News news = (News) session.get(News.class, 294912);
        session.delete(news);
        System.out.println(news);
        news.setTitle("FF");
        session.update(news); // 报错 deleted instance passed to update()
        session.saveOrUpdate(news); // 新增,没问题
    }

但是理论上来讲,一个被删除的对象,怎么还能对它进行修改或新增,或者说,被删除的对象,就不应该还有 id。
所以说,hibernate 也想到了。Hibernate 的 cfg.xml 配置文件中有一个 hibernate.use_identifier_rollback 属性, 其默认值为 false, 若把它设为 true, 将改变 delete() 方法的运行行为: delete() 方法会把持久化对象或游离对象的 OID 设置为 null, 使它们变为临时对象。

		 
        <property name="hibernate.use_identifier_rollback">trueproperty>

设置后执行如下代码:

    @Test
    public void testDelete() {
        News news = (News) session.get(News.class, 3);
        session.delete(news);
        System.out.println(news);
    }

在这里插入图片描述
此时 id 变为 null,干不了坏事儿了。

evict() 方法

evict 的作用是从 session 缓存中删除指定的持久化对象。

    @Test
    public void testEvict() {
        News news1 = (News) session.get(News.class, 2);
        News news2 = (News) session.get(News.class, 3);
        news1.setTitle("AA");
        news2.setTitle("BB");
        session.evict(news2);
    }

上述代码如果不加 evict,则会在 commit 时 flush,修改数据库表中的记录。而让 news evict 之后,news2 变成游离状态了,所以 title 是不会修改的,必须调用 update() 方法才能修改。

Hibernate 调用存储过程

Hibernate 无法直接调用存储过程,需要通过 Work 接口,得到 Connection 对象,再通过原生 JDBC 调用存储过程。Work 接口: 直接通过 JDBC API 来访问数据库的操作。

Session 的 doWork(Work) 方法用于执行 Work 对象指定的操作, 即调用 Work 对象的 execute() 方法. Session 会把当前使用的数据库连接传递给 execute() 方法。
这里我就简单的打印下 connection 就行。

    @Test
    public void testWork() {
        Work work = new Work() {
            @Override
            public void execute(Connection connection) throws SQLException {
                System.out.println(connection);
            }
        };
        session.doWork(work);
    }

在这里插入图片描述
对于批量操作,还是原生的 API 效率会更高。

你可能感兴趣的:(Hibernate,1024程序员节)