持久化类

Hibernate是持久层的ORM映射框架,专注于数据的持久化工作。

所谓的持久化,就是将内存中的数据永久存储到关系型数据库中。

所谓的持久化类指的是一个Java类与数据库表建立了映射关系,那么这个类称为是持久化类。

其实你可以简单的理解为持久化类就是一个Java类有了一个映射文件与数据库的表建立了关系。

那么我们编写持久化类的时候有哪些要求呢?

实体类编写规则

我们在编写持久化类的时候需要有一下几点需要注意:

  • 持久化类需要提供无参数的构造方法。因为在 Hibernate 的底层需要使用反射生成类的实例。

  • 持久化类的属性需要私有,对私有的属性提供公有的getset方法。

    因为在Hibernate底层会将查询到的数据进行封装。也就是说,实体类里面属性是私有的

  • 持久化类的属性要尽量使用包装的类型

    因为包装类和基本数据类型的默认值不同,包装类的类型语义描述更清晰而基本数据类型不容易描述。举个例子:

    1
    假设表中有一列员工工资,如果使用double类型,如果这个员工工资忘记录入到系统中,系统会将默认值0存入到数据库,如果这个员工的工资被扣完了,也会向系统中存入0.那么这个0就有了多重含义,而如果使用包装类类型就会避免以上情况。如果使用Double类型,忘记录入的工资就会存入null,而如果这个员工的工资被扣完了,就会存入0,不会产生歧义。
  • 持久化类要有一个唯一标识OID与表的主键对应,也就是要求实体类有属性作为唯一值(一般使用id值)

    因为Hibernate中需要通过这个唯一标识OID区分在内存中是否是同一个持久化类。在Java中通过地址区分是否是同一个对象,在关系型数据库的表中是通过主键区分是否是同一条记录。那么Hibernate就是通过这个OID来进行区分的。Hibernate是不允许在内存中出现两个OID相同的持久化对象的。

  • 持久化类尽量不要使用final进行修饰

    因为Hibernate中有延迟加载的机制,这个机制中会产生代理对象,Hibernate产生代理对象使用的是字节码的增强技术完成的,其实就是产生了当前类的一个子类对象实现的。如果使用了final修饰持久化类。那么就不能产生子类,从而就不会产生代理对象,那么Hibernate的延迟加载策略(是一种优化手段)就会失效。

持久化类我们已经可以正常编写了,但是在持久化类中需要有一个唯一标识OID与表的主键去建立映射关系。而且主键一般我们是不会让客户手动录入的,一般我们是由程序生成主键。那么Hibernate中也提供了相应的主键生成的方式,下面我们来看Hibernate的主键生成策略。

主键生成策略

主键的类型

在讲解Hibernate的主键生成策略之前,先来了解两个概念,即自然主键和代理主键,具体如下:

  • 自然主键(少见):把具体业务含义的字段作为主键,称之为自然主键。

    例如在customer表中,如果把name字段作为主键,其前提条件必须是:每一个客户的名字不允许为null,不允许客户重名,并且不允许修改客户姓名。尽管这也是可行的,但是不能满足不断变化的业务需求,一旦出现了允许客户重名的业务需求,就必须修改数据模型,重新定义表的主键,这给数据库的维护增加了难度。

  • 代理主键:把不具备业务含义的字段作为主键(一般是ID),称之为代理主键。

    该字段一般取名为”ID“,通常为整数类型,因为整数类型比字符串类型要节省更多的数据库空间。在上面的例子中,显然更合理的方式是使用代理主键。

主键生成策略

hibernate要求实体类里面有一个属性作为唯一值,对应表主键,主键可以不同生成策略

Hibernate中,提供了几个内置的主键生成策略,其常用主键生成策略的名称和描述如下:

  • 自然主键
    • assigned:自然主键生成策略. hibernate不会管理主键值.由开发人员自己录入。如果不知道 id 元素的generator属性,则默认使用该主键生成策略。
  • 代理主键
    • identity:主键自增.要求在数据库中把主键定义为自增长类型.录入时不需要指定主键.
    • sequence:Oracle中的主键生成策略.
    • increment(了解):主键自增.由hibernate来维护.每次插入前会先查询表中id最大值.+1作为新主键值.只有当没有其他进程向同一张表中插入数据时才可以使用,不能在集群环境下使用。
    • native:hilo+sequence+identity 自动三选一策略.适合跨数据库平台开发 自动增长
    • uuid:产生随机字符串作为主键. 主键类型必须为string 类型.
img

持久化对象的状态

持久化对象的三种状态

Hibernate为了更好的来管理持久化类,将持久化类分成了三种状态:瞬时态、持久态和脱管态,一个持久化类的实例可能处于三种不同状态中的某一中。

  1、瞬时态(transient)

​ 对象里面没有id值,对象与session没有关联,不处于 Session 的缓存中

  瞬时态也称为临时态或者自由态,瞬时态的实例是由new命令创建、开辟内存空间的对象,不存在持久化标识OID(相当于主键值),尚未与Hibernate的Session关联,在数据库中也没有记录,失去引用后将被JVM回收。瞬时状态的对象在内存中是孤立存在的,与数据库的数据没有任何关联,仅是一个信息携带的载体。

  2、持久态(persistent)

​ 对象里面有id值,对象与session关联,也是持久化对象

  持久态的对象存在持久化标识OID,加入到了Session缓存中,并且相关联的Session没有关闭,在数据库中有对应的记录,每条记录只对应唯一的持久化对象,需要注意的是,持久态对象是在事务还未提交前变成持久态的。

  3、脱管态(detached)

​ 对象有id值,对象与session没有关联

  脱管态也称为离线态或者游离态,当某个持久化状态的实例与Session的关联被关闭时就变成了脱管态。脱管态对象存在持久化标识OID,并且仍然与数据库中的数据存在关联,只是失去了与当前Session的关联,脱管状态对象发生改变时Hibernate不能检测到。

区分对象的三种状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void demo1() throws Exception {
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();

Customer customer = new Customer(); //瞬时态对象:没有持久化标识OID,没有被session管理
customer.setCust_name("王五");
session.save(customer); // 持久化对象:有持久化标识OID,被session管理

tx.commit();
session.close();

System.out.println(customer); //脱管态对象:有持久化标识OID,没有被session管理
}

customer对象由new关键字创建,此时还未与Session进行关联,它的状态称为瞬时态;在执行了session.save(customer)操作后,customer对象纳入了Session的管理范围,这时的customer对象变成了持久态对象,此时Session的事务还没有被提交;程序执行完commit()操作并关闭了Session后,customer对象与Session的关联被关闭,此时customer对象就变成了脱管态。

三种状态的转换

img

从图中可以看出,当一个对象被执行new关键字创建后,该对象处于瞬时态;当对瞬时态对象执行Session的save()或saveOrUpdate()方法后,该对象将被放入Session的一级缓存,对象进入持久态;当对持久态对象执行evict()、close()或clear()操作后,对象进入脱管态(游离态);当直接执行Session的get()、load()、find()或iterate()等方法从数据库里查询对象时,查询到的对象也处于持久态;当对数据库中的记录进行update()、saveOrUpdate()以及lock()等操作后,此时脱管态的对象就过渡到持久态;由于瞬时态和脱管态的对象不在Session的管理范围,所以一段时间后会被JVM回收。

持久化对象的三种状态可以通过调用Session中的一系列方法实现状态间的转换,具体如下:

瞬时态转换到其他状态

通过前面学习可知,瞬时态的对象由new关键字创建,瞬时态对象转换到其他状态总结如下:

  • 瞬时态转换为持久态:执行Session的save()或saveOrUpdate()方法。
  • 瞬时态转换为脱管态:为瞬时态对象设置持久化标识OID。

  由于持久化对象状态演化图中没有涉及到瞬时态转换到脱管态的情况,这里做下简要的说明,在前面的学习中可知,脱管态对象存在OID,但是没有Session的关联,也就是说脱管态和瞬时态的区别就是OID有没有值,所以可以通过为瞬时态对象设置OID,使其变成脱管态对象。

持久态对象转换到其他状态

持久化对象可以直接通过Hibernate中Session的get()、load()方法,或者Query查询从数据库中获得,持久态对象转换到其他状态总结如下:

  • 持久态转换为瞬时态:执行Session的delete()方法,需要注意的是被删除的持久化对象,不建议再次使用。
  • 持久态转换为脱管态:执行Session的evict()close()clear()方法。evict()方法用于清楚一级缓存中某一对象;close()方法用于关闭Session,清楚一级缓存;clear()方法用于清除一级缓存的所有对象。

脱管态对象转换到其他状态

脱管态对象无法直接获得,是由其他状态对象转换而来的,脱管态对象转换到其他状态总结如下:

  • 脱管态转换为持久态:执行Session的update()saveOrUpdate()lock()方法。
  • 脱管态转化为瞬时态:将脱管态对象的持久化标识OID设置为null

由于持久化对象状态演化图中没有涉及到脱管态转换到瞬时态的情况,这里做下简要的说明,跟瞬时态转换到脱管态的情况相似,脱管态和瞬时态的区别就是OID有没有值,所有可以通过将脱管态对象的OID设置为null,使其变成瞬时态对象。例如在session.close()操作后,加入代码customer.setCust_id(null),customer对象将由脱管态转化为瞬时态。

持久态对象能够自动更新数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 测试持久化类的持久化对象有自动更新数据库的能力
@Test
public void demo2() throws Exception {
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();

// 获得持久化对象
Customer customer = session.get(Customer.class, 1l);
customer.setCust_name("王五");

// session.update(customer); // 不用手动调用update方法j就可以更新

tx.commit();
session.close();
}

执行测试我们会发现,我们并没有手动调用update方法,Hibernate就可以将数据自动更新了。持久态对象之所以有这样的一个功能,其实都依赖了HIbernate的一级缓存。

这一部分参考 https://www.cnblogs.com/yft-javaNotes/p/10244422.html

Hibernate的一级缓存

hibernate的一级缓存默认打开的

hibernate的一级缓存使用范围,是session范围,从session创建到session关闭范围

hibernate的一级缓存中,存储数据必须 持久态数据

Hibernate的缓存分为一级缓存和二级缓存(二级缓存现在不使用了),Hibernate的这两级缓存都位于持久化层,存储的都是数据库数据的备份。其中第一级缓存为Hibernate的内置缓存,不能被卸载。接下来围绕Hibernate的一级缓存进行详细的讲解。

Hibernate的一级缓存就是指Session缓存,Session缓存是一块内存空间,用来存放相互管理的Java对象,在使用Hibernate查询对象的时候,首先会使用对象属性的OID值在Hibernate的一级缓存中进行查找,如果找到匹配OID值的对象,就直接将该对象从一级缓存中取出使用,不会再查询数据库;如果没有找到相同OID值的对象,则会去数据库中查找相应的数据。当从数据库中查询到所需数据时,该数据信息也会放置到一级缓存中。Hibernate一级缓存的作用就是减少对数据库的访问次数。

在Session接口的实现中包含一系列的Java集合,这些Java集合构成了Session缓存。只要Session实例没有结束生命周期,存放在它缓存中的对象也不会结束生命周期。所以一级缓存也被称为Session的基本缓存。

Hibernate的一级缓存有如下特点:

  • 当应用程序调用Session接口的save()update()saveOrUpdate()时,如果Session缓存中没有相应的对象,Hibernate就会自动的把从数据库中查询到的相应对象信息加入到一级缓存中去。
  • 当调用Session接口的load()get()方法,一级Query接口的list()iterator()方法时,会判断缓存中是否存在该对象,有则返回,不会查询数据库,如果缓存中没有要查询的对象,再去数据库中查询相应的对象,并添加到一级缓存中。
  • 当调用Session的close()方法时,Session缓存会被清空。

测试一级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 证明Hibernate一级缓存的存在
@Test
public void demo3() throws Exception {
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();

// 马上发送一条sql语句查询1号客户,并将数据存入缓存
Customer customer1 = session.get(Customer.class, 1l);
System.out.println(customer1);

// 没有发送sql语句,从缓存中取的数据
Customer customer2 = session.get(Customer.class, 1l);
System.out.println(customer2);

// true 一级缓存缓存的是对象的地址
System.out.println(customer1 == customer2);

tx.commit();
session.close();
}

以上代码中,第一次执行Session的get()方法获取customer1对象时,由于一级缓存中没有数据,所以Hibernate会向数据库发送一条sql语句,查询id为1的对象;当再次调用Session的get()方法获取customer2对象时,不会再发送sql语句,这是因为customer2对象是从一级缓存中获取的。

当Session对象的生命周期还没有结束的时候,在查询相同的数据对象,会现在缓存之中寻找,如果有找到就直接使用缓存中的数据.

img

接下来验证一下代码的执行结果是否和描述的一致。在Customer customer1 = session.get(Customer.class, 1l);这一行设置断点,用debug方式执行该方法,程序进入断点后点击单步跳过(F6),代码执行过System.out.println(customer1);语句后,控制台的输出结果如下:

img

从上图的输出结果可以看出,customer2对象的查询结果被直接打印了,说明第二次调用Session对象的get()方法时,没有向数据库发送select语句,而是直接从一级缓存中获取customer2对象。

之前我们介绍过,Hibernate的持久态对象能够自动更新数据库,其实就是依赖了一级缓存。那么一级缓存为什么就可以去更新数据库呢?其实是因为一级缓存的一块特殊区域——快照区。

img

HIbernate向一级缓存放入数据时,同时复制一份数据放入到Hibernate的快照中,当使用commit()方法提交事务时,同时会清理Session的一级缓存,这时会使用OID判断一级缓存中的对象和快照中的对象是否一致,如果这两个对象中的属性发送变化,则执行update语句,将缓存中的内容同步到数据库,并更新快照;如果一致,则不执行update语句。Hibernate快照的作用就是确保一级缓存中的数据和数据库中的数据一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一级缓存中的快照区:持久态对象能够自动更新数据库
@Test
public void demo4() throws Exception {
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();

Customer customer = session.get(Customer.class, 1l);
customer.setCust_name("张三");

// 比对缓存中和快照区的数据是否一致,如果一致,不更新数据库
// 如果不一致则自动更新数据库
tx.commit();
session.close();
}

Hibernate Session

Session 概述

Session 是什么

  • 持久化管理器:负责执行所有持久化操作的核心接口
  • 短生命周期对象:通常一个业务操作对应一个Session
  • 一级缓存:维护了持久化对象的缓存
  • 工作单元:代表应用程序与数据库的一次会话

Session 概述

  • Session接口是Hibernate向应用程序提供的操纵数据库的最主要的接口,它提供了基本的保存,更新,删除和加载Java对象的方法

  • Session 具有一个缓存, 位于缓存中的对象称为持久化对象, 它和数据库中的相关记录对应。Session 能够在某些时间点, 按照缓存中对象的变化来执行相关的 SQL 语句, 来同步更新数据库, 这一过程被称为刷新缓存(flush)。也叫一级缓存。

    在 Session 接口的实现中包含一系列的 Java 集合, 这些 Java 集合构成了 Session 缓存。 只要 Session 实例没有结束生命周期, 且没有清理缓存,则存放在它缓存中的对象也不会结束生命周期。Session 缓存可减少 Hibernate 应用程序访问数据库的频率。

    session缓存的示例

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void getUser(){
    User user=session.get(User.class,1);
    User user2=session.get(User.class,1);
    System.out.println(user);
    System.out.println(user2);//查询出来的两条记录完全一样
    }
  • 站在持久化的角度,Hibemate把对象分为4种状态:持久化态,临时状态,游离状态,删除状态,Session的特定方法能使对象从一个状态转换到另一个状态.

Session 核心方法

对象状态管理方法

方法 说明 对象状态变化
save() 将临时对象持久化 临时 → 持久
persist() 同save(),但无返回值 临时 → 持久
get() 立即加载对象 无 → 持久
load() 延迟加载对象 无 → 代理(持久)
update() 更新脱管对象 脱管 → 持久
merge() 合并脱管对象状态 脱管 → 持久
delete() 删除对象 持久 → 删除
saveOrUpdate() 智能保存或更新 临时/脱管 → 持久
evict() 从缓存移除对象 持久 → 脱管
clear() 清空整个缓存 所有持久 → 脱管
flush() 同步缓存与数据库 保持状态

查询方法

方法 说明
createQuery() 创建HQL查询
createSQLQuery() 创建原生SQL查询
createCriteria() 创建Criteria查询(已废弃)
byId() 通过ID加载(5.2+)

事务控制方法

方法 说明
beginTransaction() 开始事务
getTransaction() 获取当前事务

缓存管理方法

evict():移除特定对象

1
session.evict(student); // 从缓存移除该对象

clear():清空整个缓存

1
session.clear(); // 清空所有缓存对象

contains():检查对象是否在缓存

1
boolean cached = session.contains(student);

session缓存的操作方法

img
flush方法:

session会按照缓存中对象属性的变化来更新数据库中的记录,使数据库中记录和缓存中的对象保持一致,默认情况下,在以下时间点会刷新缓存:

  • 当应用程序调用Transaction.commit()方法时,该方法会先调用flush()方法,然后在 向 数据库提交事务

  • 显示的调用flush()方法时

  • 当应用程序执行一些查询(HQL, Criteria)操作时,如果缓存中持久化对象的属性已经发生了变化,会先 flush 缓存,以保证查询结果能够反映持久化对象的最新状态

flush 缓存的例外情况:

如果对象使用 native 生成器生成 OID, 那么当调用 Session 的 save() 方法保存对象时, 会立即执行向数据库插入该实体的 insert 语句.

flush()和commit()方法的区别:

flush()方法会执行一系列的sql语句,但是不会提交事物;commit()在提交事物前,会先调用flush()方法,然后再提交事物,提交事物以为这将数据库的操作永久的保存下来

测试flush方法

1
2
3
4
5
6
7
@Test
public void testFlush(){
News vo = (News) this.session.get(News.class,1);
vo.setInfo("flush()方法测试");
this.session.flush();//调用flush()方法
System.out.println(vo);
}

可以发现当调用了修改了缓存之中的对象,在调用了flush()方法之后,会执行update语句以来保证缓存之中的数据和数据库之中的数据保持同步

img

数据库之中保存的数据

img
refresh:

refresh会强制发送select语句, 以使数据库中的记录和缓存中的对象保持一致;如果在调用refresh方法前,手动的修改数据库中的记录,查询出来的结果还不是最新的,这跟数据库的数据的隔离级别是相关的,可以在配置文件中显示的修改事物的隔离级别,每一个隔离级别都对应一个整数;

1
2
3
4
5
6
7
@Test
public void testReFlush(){
News vo = (News) this.session.get(News.class,1);
/*使用断点,暂停程序,暂停程序的时候修改数据库中的数据,查看最后输出的内容是否有变化*/
this.session.refresh(vo);
System.out.println(vo);
}

在暂停的时候修改数据库中的info数据

img

但是会发现最后输出的结果并不是修改过的数据,因为可重复读的隔离级别,事务持续期间,禁止其他事务对这个字段进行更新。

img

缓存同步机制

自动同步:在以下时机会自动flush:

  • 事务提交时
  • 执行查询前(确保查询结果准确)
  • 显式调用session.flush()

手动同步:

1
session.flush(); // 强制同步缓存与数据库
evict()

从session缓存中把指定的持久化对象移除

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testEvict(){
//使用load()方法将数据库中的一个对象将在到缓存之中
News vo1 = (News)this.session.load(News.class,4);

//使用evict()方法将缓存中的对象清除
this.session.evict(vo1);
//当要使用该对象的时候,系统无法从缓存之中初始化该对象,就会出现异常
System.out.println(vo1);

}
img

Session核心方法详解

对实体类 CURD 操作

添加操作

通过调用session里面的save方法实现

save()方法最大的特点就是将一个临时对象变为持久化对象

为对象分配ID

在flush()缓存时会发送一条INSERT语句.

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testSave(){
News vo = new News();
vo.setTitle("testSave()");
vo.setInfo("测试Save");
vo.setPrice(55.0);

System.out.println(vo);
this.session.save(vo);
System.out.println(vo);
}
img
  • 在save()执行前,设置ID的方法是无效的,在save()方法执行过后修改id,会出现异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testSave(){
News vo = new News();
vo.setTitle("testSave()");
vo.setInfo("测试Save");
vo.setPrice(55.0);

//在save()方法执行前设置ID
vo.setId(12345);

System.out.println(vo);
this.session.save(vo);
// 在save()方法执行过后修改id,会出现异常
vo.setId(12345);
System.out.println(vo);
}

img

持久化对象的ID是不能够被修改的

img

通过调用 presist() 方法

persist()方法也会执行INSERT操作,与save()类似,但无返回值

在presist()方法执行之前,如果对象已经有ID值,则不会执行INSERT,而会抛出一个异常.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPersist(){
News vo = new News();
vo.setTitle("testSave()");
vo.setInfo("测试Save");
vo.setPrice(55.0);

vo.setId(12345);

System.out.println(vo);
this.session.persist(vo);
System.out.println(vo);
}

img

与save()的区别

  • 不保证立即执行INSERT(可能在flush时执行)

  • 不返回ID,需通过对象获取

  • JPA规范方法,行为更标准化

查询操作

调用session里面的get方法实现

执行get()方法会立即加载对象,session.get(实体类的class, id值)是根据 id 查询

1
2
3
4
5
6
@Test
public void testGet(){
News getVo = (News)this.session.get(News.class,1);

System.out.println(getVo.getClass().getName());
}

特点

  • 立即发出SELECT语句

  • 对象不存在时返回null

  • 返回真实对象(非代理)

调用session里面的load方法实现

延迟加载,返回代理对象

执行load方法,若不使用该对象,则不会立即执行查询操作,而返回一个代理对象.

1
2
3
4
5
6
// 返回代理对象,不立即查询
Student student = session.load(Student.class, 1L);

System.out.println("尚未查询数据库");
// 实际访问非ID属性时才触发查询
System.out.println(student.getName());

可以发现使用load()方法得到的对象在没有使用的情况下是一个代理对象,这是因为使用load()方法得到的对象,在没有使用任何属性的情况下,不会立即加载,而是加载到内存之中,在有需要的时候再加载对象.

  • 延迟加载(访问非ID属性时查询)
  • 对象不存在时抛ObjectNotFoundException
  • 返回代理对象(运行时生成子类)

若查询一个数据表中没有的记录,而且Session也没有被关闭,同时需要使用对象的时候.

  • 使用get方法查询会:返回null
  • 使用load方法查询:若不是用该对象的任何属性没有问题,若需要初始化,抛出异常.

在需要初始化代理对象之前若关闭Session. load方法可能会抛出 异常

1
2
3
4
5
6
7
@Test
public void testLoad(){
News loadVo = (News)this.session.load(News.class,1888);
this.session.close();//在使用loadVO类之前关闭session
System.out.println(loadVo);
}

抛出的异常org.hibernate.LazyInitializationException: could not initialize proxy - no Session(无法初始化代理,没有Session)

img
更新操作

自动脏检查更新

最常用方式:Hibernate自动检测变化

1
2
3
4
5
6
Student student = session.get(Student.class, 1L);
student.setName("王五"); // 修改属性
student.setEmail("wangwu@example.com");

// 无需调用update方法,提交时自动更新
tx.commit(); // 触发UPDATE语句
  • Session跟踪持久化对象的状态

  • flush时比较快照,生成UPDATE语句

调用session里面的update方法实现

首先查询,修改值,然后调用 update 显式更新脱管对象

update()方法相当于sql语句之中的Update语句

img

若更新一个持久化对象,不必显示的调用update()方法,因为在调用Transactioncommit()方法时,会调用session的flush()

更新一个游离对象,需要显示的调用update()方法,可以将一个游离对象转换为持久化对象

当 update() 方法关联一个游离对象时, 如果在 Session 的缓存中已经存在相同 OID 的持久化对象, 会抛出异常

当 update() 方法关联一个游离对象时, 如果在数据库中不存在相应的记录, 也会抛出异常.

注意:

  1. 无论要更新的游离对象是否与数据表中的记录是否一致,都会发送Update语句.
1
2
3
4
5
6
7
8
9
10
@Test
public void testUpdate(){
News temp =new News();
temp.setId(1);
temp.setTitle("update()方法测试");
temp.setPrice(550.2);
temp.setInfo("testUpdate");
temp.setPubDate(new Date());
this.session.update(temp);
}
img

如何能让update方法不再盲目的触发update语句?在.hbm.xml文件的class节点设置一个select-before-update=“true”()但通常不使用该属性

1
<class name="News" table="NEWS" select-before-update="true">
  1. 如果数据表中没有对应的记录,但还调用了update方法,会抛出异常

  2. 当update()方法关联一个游离对象时,如果在Session的缓存之中已经存在了相同的OID的持久化对象.会出现异常,因为在Session缓存中,不能够同时存在两个OID相同的对象,也就是说 同ID对象已存在于Session时会抛异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Test
    public void testUpdate(){
    //使用get()方法得到一个持久化对象,此时这个对象会保存在session的缓存之中
    News vo = (News) this.session.get(News.class,1);

    //创建一个于vo的id相同的对象
    News temp = new News();
    temp.setId(vo.getId());
    //使用update()方法关联这个temp对象
    //由于此时的temp对象和vo对象的id都是相同的
    //并且vo对象已经保存在了session之中
    //所以当使用update()方法的时候,就会出现在同一session之中存在两个id相同的对象
    this.session.update(temp);
    }

    img
  3. 应确保对象所有属性已设置(否则可能覆盖为null)

调用session里面的merge方法实现

更安全的更新方式

将脱管对象的状态复制到持久化对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Student detachedStudent = new Student();
detachedStudent.setId(1L);
detachedStudent.setName("钱七");

Session newSession = sessionFactory.openSession();
newSession.beginTransaction();

// 合并对象状态
Student persistentStudent = (Student) newSession.merge(detachedStudent);

// merge返回的是持久化对象,参数对象仍为脱管状态
System.out.println(detachedStudent == persistentStudent); // false

newSession.getTransaction().commit();
newSession.close();

方法工作流程

  1. 检查传入对象
    • 如果为 null,直接返回 null
    • 如果是持久化状态,直接返回该对象(无操作)
  2. 查找对应持久化对象
    • 在 Session 缓存中查找同 ID 的实体
    • 如果找到,将脱管对象属性值复制到该持久化对象
    • 如果未找到,从数据库加载或创建新实例
  3. 返回值处理
    • 返回的是持久化对象(可能是缓存中的或新创建的)
    • 重要:传入的参数对象不会被关联到 Session
  4. 状态同步
    • 在 flush 时,生成的持久化对象会同步到数据库

merge() 与 update() 的对比

特性 merge() update()
同ID对象存在时 合并属性值到持久化对象 抛出NonUniqueObjectException
返回值 返回持久化对象 无返回值
参数对象状态 保持脱管状态 转为持久化状态
SQL执行时机 根据flush策略 根据flush策略
级联行为 支持CascadeType.MERGE 支持CascadeType.UPDATE/SAVE_UPDATE
新对象处理 可能转为save操作 抛出TransientObjectException
性能 可能需要额外SELECT 直接操作

调用session里面的saveOrUpdate方法实现

Session 的 saveOrUpdate() 方法同时包含了 save() 与 update() 方法的功能,自动判断保存或更新

img
1
2
3
4
5
6
7
8
9
10
// 新对象(无ID) - 执行INSERT
Student newStudent = new Student();
newStudent.setName("孙八");
session.saveOrUpdate(newStudent);

// 脱管对象(有ID) - 执行UPDATE
Student detachedStudent = new Student();
detachedStudent.setId(1L);
detachedStudent.setName("周九");
session.saveOrUpdate(detachedStudent);

判断对象是不是临时对象:

  • Java 对象的 OID 为 null

  • 映射文件中为 <id> 设置了 unsaved-value 属性, 并且 Java 对象的 OID 取值与这个 unsaved-value 属性值匹配

判断逻辑

  • 对象ID为null → save()
  • 对象ID非null → update()

如果OID不为null,但数据表中还没有和其对应的记录,会抛出一个异常.

1
2
3
4
5
6
7
@Test
public void testSaveOrUpdate(){
News news = new News();
news.setId(123456);//此时数据库之中不存在此id的数据
this.session.saveOrUpdate(news);//如果调用saveOrUpdate()方法就会出现异常

}
img
删除操作

调用session中的delete() 方法

删除持久化对象,只要OID和数据表中的一条记录对应,就执行delete操作

如果OID和数据库中的一条记录保持一致,则执行删除操作,把对象从 Session 缓存中删除, 该对象进入删除状态,若OID在数据库中没有对应的记录,则抛出异常,Hibernate 的 cfg.xml 配置文件中有一个 hibernate.use_identifier_rollback 属性, 其默认值为 false, 若把它设为 true,改变 delete() 方法的运行行为: delete() 方法会把持久化对象或游离对象的 OID 设置为 null, 使它们变为临时对象,软删除

1
2
3
4
5
6
7
8
9
10
11
Student student = session.get(Student.class, 1L);

// 删除对象
session.delete(student); // 转为Removed状态

// 也可以直接删除(避免先查询)
Student studentToDelete = new Student();
studentToDelete.setId(2L);
session.delete(studentToDelete); // 按ID删除

tx.commit(); // 执行DELETE语句

注意事项

  • 删除后对象变为Removed状态
  • 关联对象需考虑级联删除
  • 批量删除建议使用HQL更高效

Session 方法的完整例子

使用 spring data jpa

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- H2 数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

应用配置 (application.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true

定义实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.persistence.*;
import lombok.Data;

@Data
@Entity
@Table(name = "students")
public class Student {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

private String email;

private Integer age;

@Version
private Integer version; // 乐观锁版本字段
}

Repository 接口

1
2
3
4
import org.springframework.data.jpa.repository.JpaRepository;

public interface StudentRepository extends JpaRepository<Student, Long> {
}

完整 Service 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.transaction.Transactional;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class StudentService {

@PersistenceContext
private EntityManager entityManager; // 相当于Hibernate Session

private final StudentRepository studentRepository;

public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}

// ========== 创建操作 ==========

/**
* 使用EntityManager的persist方法
*/
@Transactional
public Long createWithPersist(Student student) {
entityManager.persist(student);
return student.getId(); // 返回生成的ID
}

/**
* 使用Repository的save方法
*/
@Transactional
public Student createWithSave(Student student) {
return studentRepository.save(student);
}

// ========== 读取操作 ==========

/**
* 使用EntityManager的find方法(相当于get)
*/
@Transactional
public Student findById(Long id) {
return entityManager.find(Student.class, id);
}

/**
* 使用EntityManager的getReference方法(相当于load)
*/
@Transactional
public Student lazyFindById(Long id) {
return entityManager.getReference(Student.class, id);
}

/**
* 使用Repository方法查询
*/
public List<Student> findAllStudents() {
return studentRepository.findAll();
}

// ========== 更新操作 ==========

/**
* 自动脏检查更新
*/
@Transactional
public Student updateWithDirtyChecking(Long id, String newName) {
Student student = entityManager.find(Student.class, id);
student.setName(newName);
return student; // 事务提交时会自动更新
}

/**
* 使用merge方法更新脱管对象
*/
@Transactional
public Student updateWithMerge(Student detachedStudent) {
return entityManager.merge(detachedStudent);
}

// ========== 删除操作 ==========

/**
* 使用EntityManager的remove方法
*/
@Transactional
public void deleteWithRemove(Long id) {
Student student = entityManager.find(Student.class, id);
if (student != null) {
entityManager.remove(student);
}
}

/**
* 使用Repository的delete方法
*/
@Transactional
public void deleteWithRepository(Long id) {
studentRepository.deleteById(id);
}

// ========== 其他操作 ==========

/**
* 刷新对象状态
*/
@Transactional
public Student refreshStudent(Long id) {
Student student = entityManager.find(Student.class, id);
entityManager.refresh(student); // 从数据库重新加载
return student;
}

/**
* 清空持久化上下文
*/
@Transactional
public void clearPersistenceContext() {
entityManager.clear();
}

/**
* 立即刷新到数据库
*/
@Transactional
public void flushChanges() {
entityManager.flush();
}
}

测试控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/api/students")
public class StudentController {

private final StudentService studentService;

public StudentController(StudentService studentService) {
this.studentService = studentService;
}

@PostMapping
public Long createStudent(@RequestBody Student student) {
return studentService.createWithPersist(student);
}

@GetMapping("/{id}")
public Student getStudent(@PathVariable Long id) {
return studentService.findById(id);
}

@GetMapping("/lazy/{id}")
public Student getStudentLazy(@PathVariable Long id) {
Student student = studentService.lazyFindById(id);
System.out.println("Before access: Proxy object");
System.out.println("Name: " + student.getName()); // 触发实际加载
return student;
}

@GetMapping
public List<Student> getAllStudents() {
return studentService.findAllStudents();
}

@PutMapping("/{id}")
public Student updateStudent(@PathVariable Long id, @RequestBody Student student) {
student.setId(id);
return studentService.updateWithMerge(student);
}

@PatchMapping("/{id}")
public Student updateName(@PathVariable Long id, @RequestParam String name) {
return studentService.updateWithDirtyChecking(id, name);
}

@DeleteMapping("/{id}")
public void deleteStudent(@PathVariable Long id) {
studentService.deleteWithRemove(id);
}

@PostMapping("/refresh/{id}")
public Student refresh(@PathVariable Long id) {
return studentService.refreshStudent(id);
}
}

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import javax.transaction.Transactional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Transactional
class StudentServiceTest {

@Autowired
private StudentService studentService;

@Test
void testFullCrudCycle() {
// 1. 创建
Student student = new Student();
student.setName("Test Student");
student.setEmail("test@example.com");
student.setAge(20);

Long id = studentService.createWithPersist(student);
assertNotNull(id);

// 2. 查询
Student found = studentService.findById(id);
assertEquals("Test Student", found.getName());

// 3. 更新(自动脏检查)
found.setName("Updated Name");
// 不需要显式调用update方法

// 4. 验证更新
Student updated = studentService.findById(id);
assertEquals("Updated Name", updated.getName());

// 5. 删除
studentService.deleteWithRemove(id);

// 6. 验证删除
Student deleted = studentService.findById(id);
assertNull(deleted);
}

@Test
void testMergeDetachedEntity() {
// 1. 创建并持久化
Student student = new Student();
student.setName("Original");
Long id = studentService.createWithPersist(student);

// 2. 模拟脱管对象
Student detached = new Student();
detached.setId(id);
detached.setName("Detached");
detached.setAge(25);

// 3. 合并
Student merged = studentService.updateWithMerge(detached);

// 验证
assertEquals(id, merged.getId());
assertEquals("Detached", merged.getName());
assertEquals(25, merged.getAge());
}
}

Spring Data JPA 与 Hibernate Session 的关系

  1. EntityManager:JPA 标准接口,Spring Data JPA 底层使用 Hibernate 实现
    • persist() → Hibernate save()/persist()
    • merge() → Hibernate merge()
    • find() → Hibernate get()
    • getReference() → Hibernate load()
    • remove() → Hibernate delete()
  2. Repository 方法
    • save():实际执行 merge 语义
    • deleteById():先查询再删除

事务管理

  • @Transactional 注解自动管理事务边界
  • 方法结束时自动提交,异常时回滚
  • 同一个事务内共享持久化上下文

乐观锁冲突处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Transactional
public Student updateWithOptimisticLock(Long id, String newName) {
Student student = studentRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Student not found"));

student.setName(newName);

try {
return studentRepository.save(student);
} catch (ObjectOptimisticLockingFailureException e) {
// 处理并发修改冲突
throw new RuntimeException("数据已被其他用户修改,请刷新后重试");
}
}