JavaWeb 之 Hibernate 持久化类、缓存与事务
Hibernate 持久化类、缓存与事务。
Hibernate 的持久化类
什么是持久化类
持久化类:就是一个 Java 类(自己编写的 JavaBean),这个 Java 类与表建立了映射关系就可以称为是持久化类。
- 持久化类 = JavaBean + xxx.hbm.xml
持久化类的编写规则
- 提供一个无参数 public 访问控制符的构造器 – 底层需要进行反射。
- 提供一个标识属性,映射数据表主键字段 – 唯一标识 OID。数据库中通过主键,Java 对象通过地址确定对象,持久化类通过唯一标识 OID 确定记录。
- 所有属性提供 public 访问控制符的 set 或者 get 方法
- 标识属性应尽量使用基本数据类型的包装类型
区分自然主键和代理主键
创建表的时候
自然主键:对象本身的一个属性。创建一个人员表,每个人都有一个身份证号(唯一的)。使用身份证号作为表的主键,自然主键。(开发中不会使用这种方式)
代理主键:不是对象本身的一个属性。创建一个人员表,为每个人员单独创建一个字段。用这个字段作为主键,代理主键。(开发中推荐使用这种方式)
- 创建表的时候尽量使用代理主键创建表
主键的生成策略
1 | <hibernate-mapping> |
重要的是 uuid
和 native
increment
适用于 short,int,long 作为主键。不是使用的数据库自动增长机制。
- Hibernate中提供的一种增长机制。
- 先进行查询:
select max(id) from user;
- 再进行插入:获得
最大值 + 1
作为新的记录的主键。
SQL 代码如下:
1 | Hibernate: |
问题: 不能在集群环境下或者有并发访问的情况下使用。
identity
适用于 short,int,long 作为主键。但是这个必须使用在有自动增长数据库中,采用的是数据库底层的自动增长机制。
- 底层使用的是数据库的自动增长(auto_increment)。像 Oracle 数据库没有自动增长。
sequence
适用于 short,int,long 作为主键。底层使用的是序列的增长方式。
- Oracle 数据库底层没有自动增长,想自动增长需要使用序列。
uuid ★
适用于 char,varchar 类型的作为主键。
- 使用随机的字符串作为主键。
1 | /** |
查看生成的 uuid 主键:
1 | mysql> select * from t_person; |
native ★
本地策略。根据底层的数据库不同,自动选择适用于该种数据库的生成策略。(short,int,long)
如果底层使用 MySQL 数据库:相当于 identity
如果底层使用 Oracle 数据库:相当于 sequence
assigned
主键的生成不用 Hibernate 管理了。必须手动设置主键。
1 | p.setPid("abc"); |
Hibernate 持久化对象的状态
持久化对象的状态
Hibernate 为了管理持久化类:将持久化类分成了三个状态
瞬时态:Transient Object
- 没有持久化标识 OID,没有被纳入到 Session 对象的管理。
持久态:Persistent Object
- 有持久化标识 OID,已经被纳入到 Session 对象的管理。
脱管态:Detached Object
- 有持久化标识 OID,没有被纳入到 Session 对象的管理。
1 | /** |
控制台输出的结果:
1 | Hibernate: |
Hibernate 持久化对象状态的转换
瞬时态
- 获得瞬时态的对象
1 | User user = new User() |
- 瞬时态对象转换持久态
1 | save(); |
- 瞬时态对象转换成脱管态
1 | user.setId(1) |
持久态
- 获得持久态的对象
1 | get(); |
- 持久态转换成瞬时态对象
1 | delete(); // 比较有争议的,进入特殊的状态(删除态:Hibernate 中不建议使用的) |
- 持久态对象转成脱管态对象
1 | session.close()/evict()/clear(); |
脱管态
- 获得托管态对象:不建议直接获得脱管态的对象.
1 | User user = new User(); |
- 脱管态对象转换成持久态对象
1 | update();或者 |
- 脱管态对象转换成瞬时态对象
1 | user.setId(null); |
注意: 持久态对象有自动更新数据库的能力!!
持久态对象自动更新数据库:
1 | /** |
上面代码注释掉了 session.update(user);
方法,但是数据库依旧更新了,SQL 语句也正常执行了。
控制台输出结果(没调用 update,但是执行了 update 语句。原因是因为一级缓存的存在。):
1 | Hibernate: |
Hibernate 的一级缓存
Session 对象的一级缓存(重点)
什么是缓存?
其实就是一块内存空间,将数据源(数据库或者文件)中的数据存放到缓存中。再次获取的时候,直接从缓存中获取。可以提升程序的性能!
Hibernate 框架提供了两种缓存
一级缓存
- 自带的不可卸载的。一级缓存的生命周期与 session 一致。一级缓存称为 session 级别的缓存。
二级缓存(有更好的替代品)
- 默认没有开启,需要手动配置才可以使用的。二级缓存可以在多个 session 中共享数据,二级缓存称为是 sessionFactory 级别的缓存。
Session 对象的缓存概述
Session 接口中,有一系列的 java 的集合,这些 java 集合构成了 Session 级别的缓存(一级缓存),将对象存入到一级缓存中,session 没有结束生命周期,那么对象在 session 中存放着
内存中包含 Session 实例 –> Session 的缓存(一些集合) –> 集合中包含的是缓存对象!
证明一级缓存的存在,编写查询的代码即可证明
- 在同一个 Session 对象中两次查询,可以证明使用了缓存。
1 | /** |
控制台输出结果:
1 | Hibernate: |
分析: 获取对象的时候没有查询的 SQL 语句执行,说明 session 是从缓存中获取对象的。证明 session 一级缓存的存在。
Hibernate框架是如何做到数据发生变化时进行同步操作的呢?
使用 get 方法查询 User 对象
然后设置 User 对象的一个属性,注意:没有做 update 操作。发现,数据库中的记录也改变了。
利用快照机制来完成的(SnapShot)
控制Session的一级缓存(了解)
1 | Session.clear(); // 清空缓存。 |
Hibernate中的事务与并发
事务相关的概念**
什么是事务
- 事务就是逻辑上的一组操作,组成事务的各个执行单元,操作要么全都成功,要么全都失败.
- 转账的例子:扣钱,加钱。两个操作组成了一个事情!
事务的特性
- 原子性 – 事务不可分割。
- 一致性 – 事务执行的前后数据的完整性保持一致。
- 隔离性 – 一个事务执行的过程中,不应该受到其他的事务的干扰。
- 持久性 – 事务一旦提交,数据就永久保持到数据库中。
如果不考虑隔离性:引发一些读的问题
脏读 – 一个事务读到了另一个事务未提交的数据。
不可重复读 – 一个事务读到了另一个事务已经提交的 update 数据,导致多次查询结果不一致。
虚读 – 一个事务读到了另一个事务已经提交的 insert 数据,导致多次查询结构不一致。
设置隔离级别
通过设置数据库的隔离级别来解决上述读的问题
未提交读:以上的读的问题都有可能发生。
已提交读:避免脏读,但是不可重复读,虚读都有可能发生。
可重复读:避免脏读,不可重复读。但是虚读是有可能发生。
串行化:以上读的情况都可以避免。
如果想在 Hibernate 的框架中来设置隔离级别,需要在 hibernate.cfg.xml
的配置文件中通过标签来配置(默认隔离级别是 4):
1 | <property name="hibernate.connection.isolation">4</property> |
通过:hibernate.connection.isolation = 4
来配置,取值如下:
- 1 — Read uncommitted isolation
- 2 — Read committed isolation
- 4 — Repeatable read isolation
- 8 — Serializable isolation
丢失更新的问题
如果不考虑隔离性,也会产生写入数据的问题,这一类的问题叫丢失更新的问题。
例如:两个事务同时对某一条记录做修改,就会引发丢失更新的问题。
A事务和B事务同时获取到一条数据,同时再做修改
如果A事务修改完成后,提交了事务
B事务修改完成后,不管是提交还是回滚,如果不做处理,都会对数据产生影响
丢失更新问题演示
更新前的数据库:
1 | mysql> select * from t_user; |
假设 runA 方法和 runB() 方法同时对 t_user 表进行修改,同时查询到 id = 1 的用户(需加断点),对其进行修改。
1 | /** |
分析:假设 runA 方法先提交,那么数据库中的数据为:
1 | mysql> select * from t_user; |
分析:runA 提交之后,runB 提交,则此时数据库中数据为:
1 | mysql> select * from t_user; |
分析:最后数据库中的数据只是 runB 修改的结果,而 runA 修改的结果“丢失了”,这就是丢失更新的问题。
丢失更新解决方案
乐观锁
乐观锁采用 版本号
的机制来解决的。会给表结构添加一个字段 version = 0 ,默认值是 0
当 A 事务在操作完该条记录,提交事务时,会先检查版本号,如果发生版本号的值相同时,才可以提交事务。同时会更新版本号 version = 1 。
当 B 事务操作完该条记录时,提交事务时,会先检查版本号,如果发现版本不同时,程序会出现错误。
悲观锁
悲观锁采用的是数据库提供的一种锁机制,如果采用做了这种机制,在 SQL 语句的后面添加 for update 子句
- 当 A 事务在操作该条记录时,会把该条记录锁起来,其他事务是不能操作这条记录的。
- 只有当 A 事务提交后,锁释放了,其他事务才能操作该条记录
使用 Hibernate 框架解决丢失更新的问题
悲观锁
- 使用session.get(Customer.class, 1,LockMode.UPGRADE); 方法
乐观锁
- 在对应的 JavaBean 中添加一个属性,名称可以是任意的。例如:
private Integer version
; 并提供get
和set
方法。 - 在映射的配置文件中,提供
<version name="version"/>
标签即可。 - 注意 version 标签的编写位置。
1 | <class name="com.renkaigis.domain.User" table="t_user"> |
绑定本地的Session
Hibernate 框架中,使用 session 对象开启事务,所以需要来传递 session 对象,框架提供了 ThreadLocal 的方式
- 需要在
hibernate.cfg.xml
的配置文件中提供配置
1 | <!--开启绑定本地的 session--> |
- 重新编写
HibernateUtils
的工具类,使用 SessionFactory 的 getCurrentSession() 方法,获取当前的 Session 对象。并且该 Session 对象不用手动关闭,线程结束了,会自动关闭。
1 | // 业务层开事务 |
注意: 想使用 getCurrentSession()
方法,必须要先配置才能使用。
演示绑定本地 session
web层
浏览器端发送请求,同时新增两个用户。
1 | public class SaveServlet extends HttpServlet { |
业务层
业务层获取当前线程的 session,开启事务,调用 dao 保存用户,若发生异常则回滚。
线程结束后 session 自动关闭。
1 | public class UserService { |
持久层
获取当前 session,与业务层 session 保持一致,进行保存操作。
1 | public class UserDao { |
访问 Servlet,数据添加完成。
1 | mysql> select * from t_user; |