JavaWeb 之 Hibernate 持久化类、缓存与事务

Hibernate 持久化类、缓存与事务。


Hibernate 的持久化类


什么是持久化类

持久化类:就是一个 Java 类(自己编写的 JavaBean),这个 Java 类与表建立了映射关系就可以称为是持久化类。

  • 持久化类 = JavaBean + xxx.hbm.xml

持久化类的编写规则

  • 提供一个无参数 public 访问控制符的构造器 – 底层需要进行反射。
  • 提供一个标识属性,映射数据表主键字段 – 唯一标识 OID。数据库中通过主键,Java 对象通过地址确定对象,持久化类通过唯一标识 OID 确定记录。
  • 所有属性提供 public 访问控制符的 set 或者 get 方法
  • 标识属性应尽量使用基本数据类型的包装类型

区分自然主键和代理主键

创建表的时候

  • 自然主键:对象本身的一个属性。创建一个人员表,每个人都有一个身份证号(唯一的)。使用身份证号作为表的主键,自然主键。(开发中不会使用这种方式)

  • 代理主键:不是对象本身的一个属性。创建一个人员表,为每个人员单独创建一个字段。用这个字段作为主键,代理主键。(开发中推荐使用这种方式)

  1. 创建表的时候尽量使用代理主键创建表

主键的生成策略

1
2
3
4
5
6
7
8
9
10
<hibernate-mapping>
<class name="com.renkaigis.domain.User" table="t_user">
<id name="id" column="id">
<!--主键的生成策略-->
<generator class="native"/>
</id>
<property name="name" column="name" length="30"/>
<property name="age" column="age"/>
</class>
</hibernate-mapping>

重要的是 uuidnative

increment

适用于 short,int,long 作为主键。不是使用的数据库自动增长机制。

  • Hibernate中提供的一种增长机制。
  • 先进行查询:select max(id) from user;
  • 再进行插入:获得 最大值 + 1作为新的记录的主键。

SQL 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
Hibernate: 
select
max(id)
from
t_user
Hibernate:
insert
into
t_user
(name, age, id)
values
(?, ?, ?)

问题: 不能在集群环境下或者有并发访问的情况下使用。

identity

适用于 short,int,long 作为主键。但是这个必须使用在有自动增长数据库中,采用的是数据库底层的自动增长机制。

  • 底层使用的是数据库的自动增长(auto_increment)。像 Oracle 数据库没有自动增长。

sequence

适用于 short,int,long 作为主键。底层使用的是序列的增长方式。

  • Oracle 数据库底层没有自动增长,想自动增长需要使用序列。

uuid

适用于 char,varchar 类型的作为主键。

  • 使用随机的字符串作为主键。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 测试 uuid 生成主键策略
* Person 类有两个属性 String pname 和 String pid
* Person.hbm.xml 如下编写:
* <id name="pid" column="pid">
<generator class="uuid"/>
</id>
*/
@Test
public void runUUID() {
Session session = HibernateUtils.getSession();
Transaction tr = session.beginTransaction();
Person p = new Person();
p.setPname("老王");

session.save(p);
tr.commit();
session.close();
}

查看生成的 uuid 主键:

1
2
3
4
5
6
7
mysql> select * from t_person;
+----------------------------------+--------+
| pid | pname |
+----------------------------------+--------+
| ff80808163ab33ae0163ab33b2370000 | 老王 |
+----------------------------------+--------+
1 row in set (0.00 sec)

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
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
/**
* 持久化对象的状态
*/
@Test
public void run3() {
Session session = HibernateUtils.getSession();
Transaction tr = session.beginTransaction();

// 瞬时态:没有 OID 的值,也没有被 session 管理,此时 user 对象是瞬时态对象
User user=new User();
user.setName("空空");
user.setAge(18);

// 此时 user 中已经有 id 值了,默认的情况下,把 user 对象也保存到 session 的缓存中
session.save(user);
System.out.println(user.getId());
// 此时,user 是持久化对象

tr.commit();
// session 销毁,缓存没有了
session.close();
// 此时 user 对象存在 id 值,但是 session 销毁了,缓存不存在了,session 不管理 user 对象了
// user 是托管态对象
System.out.println(user.getId());
System.out.println(user.getName());
}

控制台输出的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Hibernate: 
select
max(id)
from
t_user
2 // 此时的 user 有 id 值,并且被 session 管理,证明 user 是持久态对象
Hibernate:
insert
into
t_user
(name, age, id)
values
(?, ?, ?)
2 // 此时 session 已经销毁,但是 user 还有 id 值,证明 user 是托管态对象
空空

Hibernate 持久化对象状态的转换

瞬时态

  • 获得瞬时态的对象
1
User user = new User()
  • 瞬时态对象转换持久态
1
2
3
save();
或者
saveOrUpdate();
  • 瞬时态对象转换成脱管态
1
user.setId(1)

持久态

  • 获得持久态的对象
1
2
3
get();
或者
load();
  • 持久态转换成瞬时态对象
1
delete();  // 比较有争议的,进入特殊的状态(删除态:Hibernate 中不建议使用的)
  • 持久态对象转成脱管态对象
1
session.close()/evict()/clear();

脱管态

  • 获得托管态对象:不建议直接获得脱管态的对象.
1
2
User user = new User();
user.setId(1);
  • 脱管态对象转换成持久态对象
1
2
3
update();或者
saveOrUpdate();或者
lock();
  • 脱管态对象转换成瞬时态对象
1
user.setId(null);

注意: 持久态对象有自动更新数据库的能力!!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 持久态对象自动更新数据库
*/
@Test
public void run4() {
Session session = HibernateUtils.getSession();
Transaction tr = session.beginTransaction();

// user 是持久态对象,有自动更新数据库的能力
User user = session.get(User.class, 1);
// 重新设置名称
user.setName("天才");

// session.update(user);

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

上面代码注释掉了 session.update(user); 方法,但是数据库依旧更新了,SQL 语句也正常执行了。

控制台输出结果(没调用 update,但是执行了 update 语句。原因是因为一级缓存的存在。):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Hibernate: 
select
user0_.id as id1_1_0_,
user0_.name as name2_1_0_,
user0_.age as age3_1_0_
from
t_user user0_
where
user0_.id=?
Hibernate:
update
t_user
set
name=?,
age=?
where
id=?

Hibernate 的一级缓存


Session 对象的一级缓存(重点)

什么是缓存?

其实就是一块内存空间,将数据源(数据库或者文件)中的数据存放到缓存中。再次获取的时候,直接从缓存中获取。可以提升程序的性能!

Hibernate 框架提供了两种缓存

一级缓存

  • 自带的不可卸载的。一级缓存的生命周期与 session 一致。一级缓存称为 session 级别的缓存。

二级缓存(有更好的替代品)

  • 默认没有开启,需要手动配置才可以使用的。二级缓存可以在多个 session 中共享数据,二级缓存称为是 sessionFactory 级别的缓存。

Session 对象的缓存概述

  • Session 接口中,有一系列的 java 的集合,这些 java 集合构成了 Session 级别的缓存(一级缓存),将对象存入到一级缓存中,session 没有结束生命周期,那么对象在 session 中存放着

  • 内存中包含 Session 实例 –> Session 的缓存(一些集合) –> 集合中包含的是缓存对象!

证明一级缓存的存在,编写查询的代码即可证明

  • 在同一个 Session 对象中两次查询,可以证明使用了缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 证明一级缓存的存在
*/
@Test
public void run5() {
Session session = HibernateUtils.getSession();
Transaction tr = session.beginTransaction();

User user = new User();
user.setName("天行健");

// 保存用户,user 已经存入到 session 的缓存中
// id 是生成的 id
Serializable id = session.save(user);
System.out.println(id);

// 获取对象,不会看到 SQL 语句
User user2 = session.get(User.class, id);
System.out.println(user2.getName());

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

控制台输出结果:

1
2
3
4
5
6
7
8
9
Hibernate: 
insert
into
t_user
(name, age)
values
(?, ?)
4
天行健

分析: 获取对象的时候没有查询的 SQL 语句执行,说明 session 是从缓存中获取对象的。证明 session 一级缓存的存在。

Hibernate框架是如何做到数据发生变化时进行同步操作的呢?

  • 使用 get 方法查询 User 对象

  • 然后设置 User 对象的一个属性,注意:没有做 update 操作。发现,数据库中的记录也改变了。

  • 利用快照机制来完成的(SnapShot)

控制Session的一级缓存(了解)

1
2
3
Session.clear(); // 清空缓存。
Session.evict(Object entity); // 从一级缓存中清除指定的实体对象。
Session.flush(); // 刷出缓存

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
2
3
4
5
6
7
8
mysql> select * from t_user;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 1 | 天才 | 18 |
| 2 | 空空 | 18 |
+----+-----------+------+
4 rows in set (0.01 sec)

假设 runA 方法和 runB() 方法同时对 t_user 表进行修改,同时查询到 id = 1 的用户(需加断点),对其进行修改。

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
/**
* 丢失更新1
*/
@Test
public void runA() {
Session session = HibernateUtils.getSession();
Transaction tr = session.beginTransaction();

User user = session.get(User.class, 1);
// 修改 id = 1 的用户名为白蛇
user.setName("白蛇");

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

/**
* 丢失更新2
*/
@Test
public void runB() {
Session session = HibernateUtils.getSession();
Transaction tr = session.beginTransaction();

User user = session.get(User.class, 1);
// 修改 id = 1 的用户年龄为 88
user.setAge(88);

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

分析:假设 runA 方法先提交,那么数据库中的数据为:

1
2
3
4
5
6
7
8
mysql> select * from t_user;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 1 | 白蛇 | 18 |
| 2 | 空空 | 18 |
+----+-----------+------+
4 rows in set (0.01 sec)

分析:runA 提交之后,runB 提交,则此时数据库中数据为:

1
2
3
4
5
6
7
8
mysql> select * from t_user;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 1 | 天才 | 88 |
| 2 | 空空 | 18 |
+----+-----------+------+
4 rows in set (0.01 sec)

分析:最后数据库中的数据只是 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; 并提供 getset 方法。
  • 在映射的配置文件中,提供 <version name="version"/> 标签即可。
  • 注意 version 标签的编写位置。
1
2
3
4
5
6
7
8
9
10
11
<class name="com.renkaigis.domain.User" table="t_user">
<id name="id" column="id">
<generator class="native"/>
</id>

<!--乐观锁,使用 version 标签-->
<version name="version"/>

<property name="name" column="name" length="30"/>
<property name="age" column="age"/>
</class>

绑定本地的Session

Hibernate 框架中,使用 session 对象开启事务,所以需要来传递 session 对象,框架提供了 ThreadLocal 的方式

  • 需要在 hibernate.cfg.xml 的配置文件中提供配置
1
2
<!--开启绑定本地的 session-->
<property name="hibernate.current_session_context_class">thread</property>
  • 重新编写 HibernateUtils 的工具类,使用 SessionFactory 的 getCurrentSession() 方法,获取当前的 Session 对象。并且该 Session 对象不用手动关闭,线程结束了,会自动关闭。
1
2
3
4
5
// 业务层开事务
public static Session getCurrentSession() {
// 从 ThreadLocal 类中获取到 session 对象
return FACTORY.getCurrentSession();
}

注意: 想使用 getCurrentSession() 方法,必须要先配置才能使用。

演示绑定本地 session

web层

浏览器端发送请求,同时新增两个用户。

1
2
3
4
5
6
7
8
9
10
11
12
public class SaveServlet extends HttpServlet {

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
User u1 = new User();
u1.setName("测试1");

User u2 = new User();
u2.setName("测试2");

new UserService().save(u1, u2);
}
}

业务层

业务层获取当前线程的 session,开启事务,调用 dao 保存用户,若发生异常则回滚。

线程结束后 session 自动关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserService {
public void save(User u1, User u2) {
UserDao dao = new UserDao();
Session session = HibernateUtils.getCurrentSession();
Transaction tr = session.beginTransaction();
try {
dao.save1(u1);
// int a = 10 / 0;
dao.save2(u2);
tr.commit();
} catch (Exception e) {
// 出现问题,回滚事务
tr.rollback();
} finally {
// session.close();
// 以前需要自己释放资源
// 现在 session 不用关闭,线程结束后 session 自动关闭。
}
}
}

持久层

获取当前 session,与业务层 session 保持一致,进行保存操作。

1
2
3
4
5
6
7
8
9
10
11
public class UserDao {
public void save1(User u1) {
Session session = HibernateUtils.getCurrentSession();
session.save(u1);
}

public void save2(User u2) {
Session session = HibernateUtils.getCurrentSession();
session.save(u2);
}
}

访问 Servlet,数据添加完成。

1
2
3
4
5
6
7
8
9
10
mysql> select * from t_user;
+----+---------+------+---------+
| id | name | age | version |
+----+---------+------+---------+
| 1 | 天才 | 88 | 1 |
| 2 | 空空 | 18 | 0 |
| 7 | 测试1 | NULL | 0 |
| 8 | 测试2 | NULL | 0 |
+----+---------+------+---------+
4 rows in set (0.00 sec)