Keep Calm and Carry On

Hibernate框架基础

一、Hibernate框架介绍

Hibernate是一种ORM框架,用于在java对象和关系型数据库之间建立映射,以实现直接存取java对象,以面向对象的方法操作数据库,自动生成SQL语句,简化开发。

ORM:Object Relational Mapping,对象关系映射,用于实现面向对象编程语言里不同类型系统数据之间的转换,简单来说就是对象和数据库表之间通过映射关系联系起来,一个对象对应一张数据库表。

二、Hibernate的使用

1. 导入jar包

2. HIbernate的核心配置文件

project/etc下的 hibernate.cfg.xml配置文件拷贝到src目录下,并配置如下内容:

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
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>
<!-- 1、配置数据库连接的4个参数 -->
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql:///hibernate</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">root</property>

<!-- 是否显示sql语句 -->
<property name="show_sql">true</property>
<!-- 是否格式化sql语句 -->
<property name="format_sql">true</property>
<!-- 是否自动提交事务 -->
<property name="hibernate.connection.autocommit">true</property>

<!--配置映射文件与数据库表的关系:
update:如果数据库没有表,就自动创建表(常用的值)
create:每次都会创建表,也就是会覆盖
create-drop:每次都会创建表,执行完之后就会删除表-->
<property name="hibernate.hbm2ddl.auto">update</property>


<!-- 2、配置JavaBean与表的映射文件 -->
<mapping resource="com/gyf/hibernate/domain/User.hbm.xml"/>
</session-factory>
</hibernate-configuration>

3. 编写javaBean与映射文件javaBean.hbm.xml

注意:

  1. XXX.hbm.xml里的xxx与javabean同名,比如模型为User,那么映射文件应该命名为User.hbm.xml,并且放置在同一个包里面,比如都是放在domain包下;
  2. 映射文件里,如果javabean的成员变量与数据库表字段不一致,要指定column

4. 测试

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
public class Test01 {
@Test
public void test1() {
//1.获取核心 配置文件对象,默认是加载src的hibernate.cfg.xm文件
Configuration cfg = new Configuration().configure();

//2.创建会话工厂
SessionFactory factory = cfg.buildSessionFactory();

//3.创建会话
Session session = factory.openSession();
//开启事务
Transaction trans = session.beginTransaction();

//保存
User user = new User();
user.setUsername("zhuobo");
user.setPassword("abcdefg");
session.save(user);

//提交事务
trans.commit();
//4.关闭会话
session.close();
//5.关闭工厂,释放资源
factory.close();
}
}

三、Hibernate的一些API

1. Configuration配置对象

一般来说Hibernate可是使用两种核心配置文件的格式,一种是xml格式、另一种是properties格式,这两个配置文件都是放在project/etc目录下。由于properties格式的文件是键值对的形式,有键名必须唯一的局限性,一般来说xml配置文件更加常用,可以配置更多的内容。

在上面的测试中,要先获取Configuration配置对象,事实上就是在加载配置文件:

1
2
3
4
5
//1.获取核心 配置文件对象,默认是加载src的hibernate.cfg.xm文件
//调用configure()方法加载的是hibernate.cfg.xml文件
//直接new Configuration()加载的是hibernate.properties文件
// 默认情况下,配置文件都是放在src目录先
Configuration cfg = new Configuration().configure();

加载映射文件

  1. 在hibernate.cfg.xml中配置,这是最常用的方法:
1
2
<!-- 2、配置JavaBean与表的映射文件 -->
<mapping resource="com/gyf/hibernate/domain/User.hbm.xml"/>
  1. 配置文件对象Configuration调用方法addResource():
1
2
3
//1.获取核心 配置文件对象,默认是加载src的hibernate.cfg.xm文件
Configuration cfg = new Configuration().configure();
cfg.addResource("cn\\zhuobo\\web\\domain\\User.hbm.xml");
  1. 配置文件对象Configuration调用方法addClass():
1
2
3
//1.获取核心 配置文件对象,默认是加载src的hibernate.cfg.xm文件
Configuration cfg = new Configuration().configure();
cfg.addClass(User.class);

这三种方法,第一种在核心配置文件hibernate.cfg.xml中配置映射文件时最常使用的方法。

2. SessionFactory

SessionFactory是生产session的工厂,就相当于数据库连接池,管理数据库连接。要注意的是SessionFactory不是轻量级的,一般来说一个项目只需要一个SessionFactory就可以了,用来负责管理应用程序和数据库的所有会话(session),SessionFactory是线程安全的。获取SessionFactory的方法是Configuration配置对象调用方法buildSessionFactory():

1
2
3
4
//1.获取核心 配置文件对象,默认是加载src的hibernate.cfg.xm文件
Configuration cfg = new Configuration().configure();
//2.创建会话工厂
SessionFactory factory = cfg.buildSessionFactory();

3. Session

Hibernate中的Session与HttpSession的概念有点不一样,HttpSession表示用户与服务器的一次会话,Hibernate的Session表示应用程序与数据库的一次会话(交互)。session是轻量级的,线程不安全的,通常将一个session和一个数据库事务绑定,每次执行一个数据库事务都要创建一个session(从SessionFactory中获取一个session),使用session后还有关闭session(归还session)。

SessionFactory提供了两个方法来获取Session:

  1. factory.openSession():获取一个全新的Session;

  2. factory.getCurrentSession():获取与当前线程绑定的Session,一个线程只有一个Session。

要获取与当前线程绑定的Session,要在hibernate配置文件hibernate.cfg.xml中配置一个属性:

1
<property name="hibernate.current_session_context_class">thread</property>
1
2
3
4
5
6
// currentSession1和currentSession2是同一个线程的,因此是同一个session
Session currentSession1 = factory.getCurrentSession();
Session currentSession2 = factory.getCurrentSession();

System.out.println(currentSession1.hashCode());
System.out.println(currentSession2.hashCode());

4. Transaction事务

在hibernate中获取、开启、提交、回滚事务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取事务
Transaction transaction = session.getTransaction();

// 开启事务
transaction.begin();

User user = new User();
user.setUsername("Alice");
user.setPassword("alicePassword");

session.save(user);

// 提交事务,可以中hibernate.cfg.xml中配置自动提交
transaction.commit();

// 回滚事务,在catch中写Session session = factory.openSession();

User user = (User) session.get(User.class, 3);
System.out.println(user);
transaction.rollback();

5. Session的方法

  1. get():通过id查询,如果没有返回为null;
1
2
3
4
Session session = factory.openSession();

User user = (User) session.get(User.class, 3);
System.out.println(user);
  1. load():通过Id查询,如果没没有抛出异常;

    get()和load()的区别:get方法执行的时候直接加载数据库,查询数据库,load是一种懒加载,也就是当load方法被执行的时候不直接去加载数据库,而是要使用到数据才去加载数据库,比如要访问或者更新获取到的对象的数据,才去查询数据库

  2. save():保存

  3. update():更新

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
// 1. update更新,先查询对象,再更新对象,这种更新是根据ID更新的,但是更新一个密码、连用户名也一起更新了
User user = (User) session.get(User.class, 4);
user.setPassword("bbbbb");
session.update(user);

/*
Hibernate:
select
user0_.id as id0_0_,
user0_.username as username0_0_,
user0_.password as password0_0_
from
t_user user0_
where
user0_.id=?
*/

// 2.自己创建对象,设置ID,调用saveOrUpdate():保存或者更新,如果user有id就update,没有就insert
transaction.begin();
User user = new User();
//user.setUid(9);
user.setUsername("Tom");
user.setPassword("cccc");

session.saveOrUpdate(user);

//提交事务
transaction.commit();
  1. delete():删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// delete删除数据的两种方法
// 1. 先获取对象,再删除数据,删除是根据Id删除的
transaction.begin();

User user = (User) session.get(User.class, 3);
System.out.println(user);

session.delete(user);
// 提交事务
transaction.commit();


// 2. 由于知道delete是根据id删除数据,根据ID写SQL语句,因此可以直接设置一个对象的ID,不必查询
// 开启事务
transaction.begin();

User user1 = new User();
user1.setUid(9);
session.delete(user1);

// 提交事务
transaction.commit();

6 .Query查询对象

HQL:Hibernate Query Language,hibernate查询语言,是一种面向对象,面向对象的数据的查询语言(SQL面向数据库,依赖数据库,),实际上HQL被hibernate翻译为SQL来执行。

  1. 查询一行数据:
1
2
3
4
5
6
7
Query query = session.createQuery("from User where username = ? and password = ?");
query.setParameter(0, "zhuobo");
query.setParameter(1, "pass");

// 结果是单行数据
User user = (User) query.uniqueResult();
System.out.println(user);
  1. 分页查询:
1
2
3
4
5
6
7
8
9
10
11
// 分页查询,相当于limit 3, 3
Query query = session.createQuery("from User");
query.setFirstResult(3);
query.setMaxResults(3);

// 结果是多行数据
List list = query.list();

for (Object o : list) {
System.out.println(o);
}

7. Criteria查询对象

QBC:query by Criteria,hibernate提供的纯面向对象的查询语言;

  1. 查询
1
2
3
4
5
6
7
8
// eq =
// gt >
// ge >=
Criteria criteria = session.createCriteria(User.class);
criteria.add(Restrictions.eq("username", "zhuobo"));
criteria.add(Restrictions.eq("password", "pass"));
User user = (User) criteria.uniqueResult();
System.out.println(user);
  1. 模糊查询
1
2
3
4
// 模糊查询
criteria.add(Restrictions.like("username", "%bo%"));
List<User> list = criteria.list();
System.out.println(list);

8. SQL查询对象

使用原生的SQL进行查询。

1
2
3
4
5
6
7
8
// 查询返回的是一个集合,集合里面装的是数据
List<Object[]> list = sqlQuery.list();
for (Object[] objects : list) {
for (Object object : objects) {
System.out.println(object);
}
System.out.println("----------");
}

9. 抽取一个hibernate工具类

为了简化hibernate的书写,抽取一个工具类。该工具类应该提供获取session的方法,提供关闭会话工厂的方法。

创建一个工具类 cn.zhuobo.web.utils.HibernateUtils.java:

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
public class HibernateUtils {
private static SessionFactory factory;

// 静态代码块,类加载的时候执行一次,用来读取hibernate的配置文件
static {
// 获取核心配置文件
Configuration configure = new Configuration().configure();
// 创建会话工厂
factory = configure.buildSessionFactory();

// 监听程序结束,监听到程序结束就关闭会话工厂
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("程序结束了-----------");
factory.close();
}
});
}

// 获取session
public static Session openSession() {
return factory.openSession();
}

// 获取当前与当前线程关联的session
public static Session getCurrentSession() {
return factory.getCurrentSession();
}
}

四、xxx.hbm.xml映射文件

1. 实体类的编写规则

  1. 提供无参数的构造方法;
  2. 提供一个标识属性,映射数据库表的主键字段,也就是要提供一个ID;
  3. 所有的属性都要提供set、get方法;
  4. 用到基本数据类型时,尽量使用基本数据类型的包装类
  5. 不要使用final修饰实体,否则该实体无法被继承,无法生成代理对象进行优化;

2. 持久化对象的唯一标识 OID

OID也就是对象的ID:

  1. java是根据对象的地址区分同一个类的不同对象的;
  2. 关系型数据库根据主键区分不同的记录;
  3. hibernate使用OID来建立内存中的对象和数据库中的记录的对应关系:也就是对象的OID和数据库的主键的对应关系;
  4. 为了保证OID的唯一性,一般来说对象的ID不人为地赋值,让hibernate为OID赋值;

3. 自然主键和代理主键

  1. 主键条件:非空、不重复、不可改变;
  2. 自然主键:在业务中,某个属性符合主键的三个条件的要求,那么这个属性可以作为主键列(比如用户名);
  3. 代理主键:如果在业务中没有符合主键三个条件的属性,那么可以增加一个没有意义的属性作为主键;

4. 基本数据类型和包装类

  1. 基本数据类型和包装类对应的hibernate的映射的类型是相同的;
  2. 基本数据类型无法表示null,如数字类型的默认值是0;
  3. 包装类型的默认值是null

5. 主键的生成策略

  1. native:mysql数据库中,id自动增长;最常用的规则;
  2. increment:也是自动增长,但是会执行select max(id)...,根据最大的id进行增长;
  3. sequence:一般在Oracle数据库中使用;
  4. hilo:hibernate自己的id规则,一般不使用;
  5. uuid:长字符串,表示ID的类型是字符串类型,需要把模型(实体)的id改为字符串类型,在保存的时候不需要自己设置id,hibernate自动为对象设置id;最常使用
  6. assigned:需要自己设置id;最常使用
1
2
3
<id name="uid" column="id">
<generator class="native"></generator>
</id>
1
2
3
4
5
6
7
8
9
10
11
session.beginTransaction().begin();

User user = new User();
user.setUsername("zhuobodd");
user.setPassword("zhuobodd_password");
user.setGender("male");
// 主键的生成策略为assigned,要自己设置id
user.setUid(UUID.randomUUID().toString().replace("-", ""));
session.save(user);

session.beginTransaction().commit();

6. 普通属性

1. 动态插入

当没有配置动态插入的时候,插入一个对象的SQL语句会操作这个对象的所有属性,无论该属性是否为null

  1. 配置动态插入:
1
2
3
4
5
6
7
8
9
10
<!-- 动态插入:dynamic-insert="true"-->
<class name="cn.zhuobo.web.domain.User" table="t_user" dynamic-insert="true">
<id name="uid" column="id">
<generator class="native"></generator>
</id>

<property name="username"></property>
<property name="password"></property>
<property name="gender"></property>
</class>
  1. 配置动态插入前的SQL:
1
2
3
4
5
6
7
-- 在配置动态插入前,哪怕对象只有一个属性是有值的,SQL语句都是操作所有的属性
insert
into
t_user
(username, password, gender)
values
(?, ?, ?)
  1. 配置动态插入后的SQL:
1
2
3
4
5
6
7
-- 配置动态插入后,由于只有属性username有值,因此只插入username属性,空字段不显示
insert
into
t_user
(username)
values
(?)

2. 动态更新

与动态插入类似,配置动态更新之前,一旦更新对象的某个属性,就会更新全部属性。比如:现在只更新了一个username属性,但是hibernate自动生成的SQL语句还是更新了全部的属性:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test3() {
// 动态更新、动态插入

Session session = HibernateUtils.openSession();
session.beginTransaction().begin();

User user = (User) session.get(User.class, 1);
user.setUsername("zhuoboaa");

session.beginTransaction().commit();
}

生成的SQL语句:

1
2
3
4
5
6
7
8
9
Hibernate: 
update
t_user
set
username=?,
password=?,
gender=?
where
id=?

xxx.hbm.xml配置动态更新:dynamic-update="true"

1
2
3
4
5
6
7
8
9
10
11
<hibernate-mapping>
<class name="cn.zhuobo.web.domain.User" table="t_user" dynamic-insert="true" dynamic-update="true">
<id name="uid" column="id">
<generator class="native"></generator>
</id>

<property name="username"></property>
<property name="password"></property>
<property name="gender"></property>
</class>
</hibernate-mapping>

生成的SQL语句只更新需要更新的一个属性值,没有更新的属性就没有必要写入SQL语句:

1
2
3
4
5
6
7
Hibernate: 
update
t_user
set
username=?
where
id=?

4. 类型的使用

  1. type:可以设定数据库表字段的类型:
1
2
3
4
5
6
7
8
<property name="username" type="java.lang.String" length="16"></property>
<property name="password" length="16"></property>
<property name="gender"></property>

<!--date:只显示年月日
datetime:显示年月日、时分秒
time:只显示时分秒-->
<property name="birthday" type="date"></property>
  1. length:可以设定数据库表字段的长度:
1
<property name="password" length="16"></property>

五、hibernate实体的状态

1. 实体状态介绍

实体有三种状态:瞬时状态、持久状态、托管状态:

  1. 瞬时状态:transient、session中没有缓存、数据库中也没有记录、OID也没有值;
  2. 持久状态:persistent、session中有缓存、数据库中有记录、OID也有值;
  3. 脱管状态/游离状态:detached、session没有缓存、数据库中有记录、OID有值;
  4. session中可以存储数据(也就是session的缓存),相当于一个id为键、对象为值的键值对的形式;

2. 瞬时状态转为持久状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test4() {
Session session = HibernateUtils.openSession();

session.beginTransaction().begin();

// 创建了一个对象,这个对象就是处于瞬时状态
// 现在该对象session中没有缓存、数据库中没有记录、OID也没有值
User user = new User("zhuobo", "password");
System.out.println(user);
// 输出该对象,瞬时状态的对象,注意到uid是没有值的
// User{uid=null, username='zhuobo', password='password', gender='null'}

// 经过save、saveOrUpdate持久化对象后,对象处于持久状态
// 现在该对象session中有缓存、数据库中有记录、OID有值
session.save(user);
System.out.println(user);
// 持久化状态的对象,注意到uid是有值的
// User{uid=3, username='zhuobo', password='password', gender='null'}

session.beginTransaction().commit();
session.close();

}

3. 持久状态转为脱管状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test5() {
Session session = HibernateUtils.openSession();

session.beginTransaction().begin();

// 获取一个持久状态的对象
User user = (User) session.get(User.class, 1);
System.out.println(user);

// 当再次获取这个对象,ID仍旧是1,这个对象已经保存在session缓存中,不用再执行SQL语句从数据库中获取
// 脱管状态、或者说是游离状态、指的是对象在session中没有缓存,数据库中有记录、有OID

// 清空session中的数据
session.clear();
// sesssion.evict(object),清楚session中指定的对象

// 这样id为1的对象就不在session中缓存了,该对象为脱管状态、要再次执行SQL语句才可以获取对象
User user1 = (User) session.get(User.class, 1);
System.out.println(user1);

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

六、一级缓存

1. 概念

一级缓存是session级别的缓存,当获得一次session(会话), hibernate在session中创建多个map集合,用于存放PO对象,为程序优化服务(比如session缓存中有的数就不需要去数据库查询),当需要用到某些数据,hibernate优先从session中查找,再去数据库查询。当session关闭时,一级缓存就被销毁。

2. 优先从一级缓存取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test5() {
Session session = HibernateUtils.openSession();

session.beginTransaction().begin();

// 获取一个持久状态的对象,需要执行SQL语句
User user = (User) session.get(User.class, 1);
System.out.println(user);


// 一级缓存已经有数据,不需要执行SQL语句
User user1 = (User) session.get(User.class, 1);
System.out.println(user1);

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

3. 清除缓存

  1. session.clear():清除缓存中所有数据;
  2. session.evict(object):清除缓存中指定的对象;

4. 一级缓存快照

快照机制:hibernate向一级缓存中存放数据时,同时向快照存放一份同样的数据备份,当使用commit()提交事务时,使用OID判断一级缓存的对象和快照的对象是否一致,如果有缓存中对象的属性发生了变化,那么就执行update语句,将缓存中的数据更新到数据库,并且更新快照的数据。如果一级缓存的数据和快照的一致,那么就不执行update语句。即,快照是为了维护一级缓存数据与数据库数据的一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test6() {
Session session = HibernateUtils.openSession();

session.beginTransaction().begin();

// user对象存储到一级缓存,同时存储到hibernate快照
User user = (User) session.get(User.class, 1);

// 缓存中的对象属性被改变,和快照中对象不一致
user.setUsername("abc");

// 事务提交,依据OID判断对象一级缓存和快照是否一致,如果不一致就更新缓存到数据库
// 在提交事务前,可以使用flush(),强制刷新缓存数据到数据库
session.flush();

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

注意:HQL会对数据进行一级缓存(数据存储到session),SQL不会对数据进行一级缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test6() {
Session session = HibernateUtils.openSession();

session.beginTransaction().begin();

// HQL查询表中所有数据,这些数据都会被缓存
Query query = session.createQuery("from User");
List list = query.list();

// 这里不会执行select语句,因为有一级缓存,从缓存中获取,而不是数据库
User user = (User) session.get(User.class, 1);


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

七、一些方法的区别

1. save和persist

  1. save方法和persist都是用来持久化对象的,保存数据到数据库;
  2. 执行save方法前可以人为地设置id,但是保存时会自己设置的id会被忽略,使用hibernate生成的id;
  3. 执行persist方法前不可以人为地设置id,设置会报错;

2. update

如果OID存在就更新对象,不存在就报错;

3. saveOrUpdate

判断OID是否存在,如果不存在就执行insert语句,否则执行update语句;

八、 hibernate多表关联关系映射

1. 多表关系

一对一:

一对多:主表的主键、从表的外键形成主外键关系

多对多:提供中间表,该中间表提供两个外键(对应两个主表的主键);

2. 一对多、多对一的例子

客户和订单的关系是一个一对多的关系(订单和客户的关系是一个多对一的关系),hibernate要描述这种关系需要按照以下的规则:

  1. 编写客户、订单实体类
  1. 为实体类编写映射文件

Customer.hbm.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.zhuobo.web.domain">
<class name="Customer" table="t_customer">
<id name="id" column="id">
<generator class="native" />
</id>

<property name="name" column="name" length="20" />

<!--Customer是多对一关系中一的一方,使用set标签描述多的一方-->
<set name="orders">
<!--column指的是order表中的外键-->
<key column="customer_id" />
<one-to-many class="Order" />
</set>
</class>

</hibernate-mapping>

Order.hbm.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.zhuobo.web.domain">
<class name="Order" table="t_order">
<id name="id" column="id">
<generator class="native" />
</id>

<property name="name" length="20"/>

<!--order是多对一中多的一方
-->
<many-to-one name="customer" column="customer_id" class="Customer" />

</class>
</hibernate-mapping>

生成的两张表:

  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
/**
* 多表映射
*/
@Test
public void test7() {
Session session = HibernateUtils.openSession();
session.beginTransaction().begin();

// 1. 创建客户、订单
Customer customer = new Customer("Hugh");
Order order1 = new Order("1984");
Order order2 = new Order("国富论");

// 2. 维护订单和客户的关系
order1.setCustomer(customer);
order2.setCustomer(customer);

// 3. 维护客户和订单的关系
customer.getOrders().add(order1);
customer.getOrders().add(order2);

// 4. 先保存客户,客户保存有才有id,订单表的外键就是客户id,一共执行5条SQL语句
session.save(customer);// 执行3条insert语句
session.save(order1); // 执行更新订单的customer_id的update语句
session.save(order2); // // 执行更新订单的customer_id的update语句

session.beginTransaction().commit();
session.close();
}
  1. 设置外键维护的方式

Customer的映射文件Customer.hbm.xml中,set标签指定属性inverse = "false",表示由Customer来维护双发的关系,值为 true表示由对方来维护关系。

1
2
3
4
5
<set name="orders" inverse="true">
<!--column指的是order表中的外键-->
<key column="customer_id" />
<one-to-many class="Order" />
</set>

指定由谁来维护关系(维护两个表之间的关系),在多对多的关系中由谁来维护在效率上差别不大,在一对多的关系中,如果指定了一方来维护关系,那么但删除或者插入一方的一条记录,就要更新与这条记录相关的多方的所有记录;如果指定了多方来维护关系,那么当删除获取插入一方的记录,由于关系就是由多方来维护,就不会有update操作,就只是删除或者插入多方的对象即可。即,在一对多的关系中,应当将关系交给多方维护,会更加高效、更加直观。

  1. 多对一关系中的删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test9() {
Session session = HibernateUtils.openSession();
session.beginTransaction().begin();

Customer customer = (Customer) session.get(Customer.class, 2);

// 要删除一对多的一的一方,不能直接删除,因为有另一个表的数据和这条记录关联
// 先将多的一方的与该记录先关联的外键设为null,再删除
for (Order order : customer.getOrders()) {
order.setCustomer(null);
}

// 删除
session.delete(customer);

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

3. cascade级联

级联,也就是串联,但操作A时,同时也会操作B。

在配置级联之前,如果一个Customer有两个order,那么要存储这三个数据到数据库就要调用3save方法;当配置了级联之后,只需显式地存储一个对象,另外两个对象会自动存储。

配置级联之前:需要显式地存储3个对象

1
2
3
4
// 4. 先保存客户,客户保存有才有id,订单表的外键就是客户id,一共执行5条SQL语句
session.save(customer);// 执行3条insert语句
session.save(order1); // 执行更新订单的customer_id的update语句
session.save(order2); // // 执行更新订单的customer_id的update语句

配置级联:在映射文件中,set标签内配置

1
2
3
4
5
6
<!--Customer是多对一关系中一的一方,使用set标签描述多的一方-->
<set name="orders" inverse="true" cascade="save-update">
<!--column指的是order表中的外键-->
<key column="customer_id" />
<one-to-many class="Order" />
</set>

cascade属性的取值:

  1. save-update:保存、更新数据都采用级联操作的方法;
  2. delete:删除操作采用级联的方法,比如在配置级联删除以前,单独删除Customer会报错,配置会删除与之关联的order,删除Customer;
  3. delete-orphan:孤儿删除,先解除Customer和与之相关的order的关系,删除B(order)但是还保留A(customer);
  4. 这些属性的值可以组合使用,用逗号,分隔,all(表示save-updatedelete组合),all-delete-orphan(表示save-updatedeletedelete-orphan三个组合);
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
// 孤儿删除 delete-orphan
/*
1. 配置级联为delete-orphan
2. 将订单从customer的set集合中删除
*/

@Test
public void test9() {
Session session = HibernateUtils.openSession();
session.beginTransaction().begin();

Customer customer = (Customer) session.get(Customer.class, 8);

// 要删除一对多的一的一方,不能直接删除,因为有另一个表的数据和这条记录关联
// 先将多的一方的与该记录先关联的外键设为null,再删除

// 1. 解除customer和两个order的关系
// 从customer的set中删除order
Set<Order> orders = customer.getOrders();
Iterator<Order> iterator = orders.iterator();
// 循环移除订单
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}

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

4. 多对多的例子

学生和课程是一种多对多的关系,一个学生可以选择多门课程、一门课程也可以被多个学生选择,配置过程可一对多类似。

1. 编写实体类

2. 编写映射文件

首先明确多对多的关系需要一张中间表来维护,中间表的两个字段分别关联学生表、课程表,也即是学生表、课程表的两个外键。

  1. Student.hbm.xml
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
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<!--指定包名-->
<hibernate-mapping package="cn.zhuobo.web.domain">
<!--指定实体类,以及实体类对应的表名-->
<class name="Student" table="t_student">
<!--配置这张表的主键,有且只有一个-->
<id name="id">
<generator class="native" />
</id>

<!--其他的属性,用property节点-->
<property name="name" length="20"/>

<!--配置多对多的关系,需要一张中间表
inverse="false":由学生表来维护关系
table:中间表表名
cascade:级联,存储的时候只要存储学生
-->

<set name="courses" table="t_student_course" cascade="all">
<!--key:这个实体在中间表的字段名-->
<key column="sid"/>
<!--many-to-many:class指定另一方实体,column指定另一方在中间表的字段名-->
<many-to-many class="Course" column="cid"/>
</set>
</class>
</hibernate-mapping>
  1. Course.hbm.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.zhuobo.web.domain">
<class name="Course" table="t_course">
<id name="id">
<generator class="native" />
</id>

<property name="name" length="20"/>

<set name="students" table="t_student_course">
<key column="cid"/>
<many-to-many class="Student" column="sid"/>
</set>
</class>

</hibernate-mapping>

3. 测试

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
@Test
public void test1() {
Session session = HibernateUtils.openSession();
session.beginTransaction().begin();
// 1. new 学生 课程
Student student1 = new Student("Trumpt");
Student student2 = new Student("Putin");

Course java = new Course("Java");
Course python = new Course("Python");

/* 因为配置的是学生表来维护关系,而且在学生表配置了级联,因此只需要
为学生添加课程,只保存学生即可
*/
// 2. 为学生添加课程
student1.getCourses().add(java);
student1.getCourses().add(python);

student2.getCourses().add(java);
student2.getCourses().add(python);

// 3. 保存学生
session.save(student1);
session.save(student2);

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

5. 一对一的例子之外键方法

学校和学校的地址是一对一的关系,一个学校一般来说就一个地址,一个地址上只要一个学校。注意:一对一事实上就是特殊的多对一,多对一是在多的一方提供联系一的一方的外键,而这个外键不是唯一的,便形成了多对一。而还是在某一方提供一个联系另一方的外键,而这个外键是唯一的,便可以形成一对一。

1. 编写实体类:

2. 编写映射文件

  1. School.hbm.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.zhuobo.web.domain">
<class name="School" table="t_school" >
<!--配置主键-->
<id name="id">
<generator class="native" />
</id>
<property name="name" length="50"/>
<!--配置一个一对一,外键在地址表上-->
<one-to-one name="address" class="Address" property-ref="school"/>

</class>
</hibernate-mapping>
  1. Address.hbm.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.zhuobo.web.domain">
<class name="Address" table="t_address">
<!--配置主键-->
<id name="id">
<generator class="native" />
</id>
<property name="name" length="20"/>
<!--配置一对一,对于地址来说,一对一就是特殊的多对一,只是关联学校的外键是唯一的
column:外键名
unique = "true":表示外键是唯一的,形成一对一
-->
<many-to-one name="school" class="School" column="school_id" unique="true"/>
</class>
</hibernate-mapping>

3. 测试

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 test1() {
Session session = HibernateUtils.openSession();
session.getTransaction().begin();
School school1 = new School("Havard University");
School school2 = new School("Sun Yat-Sen University");
Address address1 = new Address("Boston");
Address address2 = new Address("Guangzhou");

// school1.setAddress(address1);
// school2.setAddress(address2);
// 外键在address表,所以应该是给地址添加学校,否则就会出现外键为null的情况
address1.setSchool(school1);
address2.setSchool(school2);

session.save(school1);
session.save(school2);
session.save(address1);
session.save(address2);

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

6. 一对一的例子之主键方法

与上一个方法不同,这种方法的重点在于不用再address表额外添加一个外键字段,因为是一对一,而且school表的id也是唯一的,故而可以将school表的主键字段id作为address表的主键,同时也是school表在address表的外键。

1. 编写实体类

实体类和上面的添加一个外键列的实体类一样。

2. 编写映射文件

实体类School的映射文件和上面的一样,主要是修改Address的映射文件,要将Address的主键设置为School的外键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="cn.zhuobo.web.domain">
<class name="Address" table="t_address">
<id name="id">
<!--配置地址表的主键,地址表的主键又是外键-->
<generator class="foreign">
<param name="property">school</param>
</generator>
</id>
<property name="name" length="50" />
<one-to-one name="school" class="School" constrained="true" />
</class>
</hibernate-mapping>

7. 加载策略

加载策略也即是从数据库中加载数据的策略。

1. 类级别的加载策略

类级别指的是映射文件的在class标签配置lazy的加载策略,lazy属性为false表示不要懒加载,值为true表示要懒加载。

  1. get:即时加载,get方法一执行就执行SQL语句查询数据库;
  2. load:懒加载,load方法执行后不会立即执行SQL语句查询数据库,当需要用到非OID之外的属性才会去加载数据库;
  3. 配置懒加载:在映射文件中的class节点中配置属性lazy="false",表示不懒加载,那么load方法效果与get方法一样。

2. set关联级别的加载策略

set关联级别的加载策略指的是一个对象关联了一个集合,比如一个student关联一个课程的set集合,在set标签配置lazy的加载策略;默认情况下是懒加载,也就是在默认情况下,加载一个student不会去加载学生拥有的课程,当要使用到课程才会去数据库加载课程。

默认情况,懒加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 关联级别的加载
*/
@Test
public void test3() {
Session session = HibernateUtils.openSession();

// 查询ID为2的学生,由于是懒加载,不会去加载学生的courses属性(set类型)
// 当用到courses属性才去执行SQL语句加载数据库(查找课程表)
// 在分割线之后才执行查询课程表的SQL语句
Student student = (Student) session.load(Student.class, 2);

System.out.println(student);

System.out.println("------分割线------");

// 现在用到了courses属性,才去加载课程表,学生的课程数据
System.out.println(student.getCourses());

}

在set标签配置lazy属性,值为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test3() {
Session session = HibernateUtils.openSession();
/*
执行查询学生的SQL语句的同时,也会去执行查询该学生对应的课程的的SQL语句
在分割线之前就去查询课程表
*/
Student student = (Student) session.load(Student.class, 2);
System.out.println(student);

System.out.println("------分割线------");

session.close();

}

3. set中的fetch

fetch:可以指定关联的对象获取的方式,默认是select,此外还有joinsubselect,以一对多为例:

select:【默认】,普通的先查询对象,再查询关联对象,产生1+n条SQL语句;

join:使用外连接表的查询,产生1条SQL语句;

subselect:使用子查询;

4. 多对一的加载策略

上面的set标签里面的加载策略在many-to-one中也you类似的加载策略,但是会略有不同。

5. 批量加载

批量加载指的是可以指定一次加载多少数据,比如在一对多中,配置了批量加载后就可以指定一次加载多少数据。

九、常用配置

1. 整合c3p0连接池

  1. 把c3p0的jar包导入项目
  1. 配置c3p0

查看c3p0的默认配置,在文件hibernate.properties中,查找c3p0相关默认配置

hibernate.properties文件中关于c3p0的默认配置:

  1. 在hibernate核心配置文件 hibernate.cfg.xml中配置相关内容
  1. 使用,获取session,出现以下说明配置成功

2. 事务的隔离级别

脏读:一个事务访问数据并修改了数据,另一个事务读取到了没有提交的数据;

不可重复读:在一个事务内,两个读取的数据不一致(这是因为两次读取之间有另一个事务对数据作出修改),针对insertupdate操作;

幻读:一个事务对表中的全部数据作出了修改,而此时还有另一个事务insert或者delete一条记录,那么第一个事务就会发现还有数据没有被修改(或者数据少了),就像产生了幻觉一样,针对insertdelete操作;

事务的四个隔离级别

事务隔离级别 问题
read uncommitted 读未提交,存在脏读、不可重复读、虚读3个问题
read committed 读已提交,解决脏读问题,存在2个问题,
repeatable read 解决脏读、不可重复读问题,还存在1个问题,MySQL默认
serializable 串行化,没有任何问题,就是慢

hibernate中配置事务的隔离级别:在核心配置文件hibernate.cfg.xml中配置

1
2
3
4
5
6
7
<!-- 隔离级别:
1:读未提交
2:读已提交
4:可重复读
8:串行化
-->
<property name="hibernate.connection.isolation">4</property>

3. 悲观锁

悲观锁和乐观锁是多用户环境并发控制的两种锁机制,悲观锁有强烈的独占和排他性,对数据被外界修改持保守态度,在整个数据处理过程,将数据锁定。悲观锁假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作;

悲观锁分为读锁(共享锁)、写锁(排他锁):

  1. 读锁(共享锁):读锁可以被线程共享,多个线程都可以用同一把读锁读取数据

    MySQL添加读锁:select * from table lock in share mode;

1
2
3
4
5
A 终端开启事务,添加读锁
B 终端开启事务,添加读锁 # 现在共享锁在B
A 终端更新数据 # 共享锁在B,更新数据被阻塞
B 提交事务 # B提交事务,释放共享锁,A得以完成更新操作
A 提交事务
  1. 写锁(排他锁):写锁不能被共享,只要有人为数据添加了写锁,其他线程不能为数据添加其他锁,知道该线程提交数据,其他的线程都不能读取数据;

    MySQL添加写锁:MySQL可以为一张表、或者一行数据添加写锁:

1
2
select * from table for update; # 为整张表添加一个写锁
select * from table where id = 9 for update; # 为一行数据添加一个
  1. Hibernate添加写锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test1() {
// 排他锁
Session session = HibernateUtils.openSession();
session.beginTransaction();


// get 方法,添加参数 LockOptions.UPGRADE 即可为数据添加写锁
Customer customer1 = (Customer) session.get(Customer.class, 9, LockOptions.UPGRADE);
System.out.println(customer1);

// HQL添加写锁
Query query = session.createQuery("FROM Customer where id = ?");
query.setParameter(0, 9);
query.setLockOptions(LockOptions.UPGRADE);
Customer customer2 = (Customer) query.uniqueResult();
System.out.println(customer2);
}

4. 乐观锁

乐观锁就是在表中添加一个version字段来控制数据的不一致性,也就是通过查看数据版本的方法去判断数据是否是脏数据。在hibernate中,记录每次被更新,version字段的值都会加1。一个线程当前获取的一条记录的version字段值为a,如果再次查看该字段发现已经比a大了,说明该记录已经被别人修改,是脏数据。乐观锁与悲观锁的不同点在于,对于数据没有被修改很乐观,假设不会发生并发冲突,因此不会上锁,只是在提交数据更新的时候根据version字段判断一下数据是否在自己操作期间被修改了。

  1. 为实体类添加version属性
  1. 映射文件配置version节点
  1. 生成的表格有version字段

注意:

  1. 悲观锁是MySQL底层自己实现的,乐观锁只是加了一个version字段,是hibernate实现的;
  2. 乐观锁可能出现的问题:如果前端要更新数据,但是可能不会去更新version字段,导致其他的用户不知道数据被更新过了;
  3. 悲观锁适用于写入操作频繁的场景,因为如果为频繁的读取操作加锁将会增加开销,降低吞吐率;
  4. 乐观锁适用于读取操作频繁的场景,因为频繁的写入操作会造成冲突的概率增大,不断地尝试重新获取数据,增加大量的查询操作,降低吞吐率;

5. 整合log4f

  1. 导入log4j的jar包;
  2. log4j.properties导入项目的src目录:
  1. 配置日志

log4f.properties配置文件:

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
### direct log messages to stdout ###
# 数据显示在控制台
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# 日志输出布局(格式)
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### direct messages to file hibernate.log ###
# 数据写到指定的文件
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=C:\\Users\\zhuobo\\Desktop\\test_log\\hibernate.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

### set log levels - for more verbose logging change 'info' to 'debug' ###
### 日志记录器 ###
#log4j 日志级别 :
# fatal 致命错误
# error 错误
# warn 警告
# info 信息
# debug 调试信息
# trace 堆栈信息 (由高到底顺序)
# 日志级别:info表示比info高级的日志都会输出
# 输出源1:stdout表示输出源是控制台
# 输出源2: file输出到指定的文件
log4j.rootLogger=info, stdout, file

十、二级缓存

在hibernate中,一级缓存是Session级别的缓存、在一次会话(一次和数据库的交互)中共享数据。二级缓存是SessionFactory级别的缓存,SessionFactory是非轻量级的,一个应用程序共享一个会话工厂,也就是共享一个二级缓存。

SessionFactory缓存分为两个部分:

  1. 内置缓存:使用一个Map,用于存放配置信息,预定义的HQL语句等,提供给Hibernate框架自己使用的,对外只读,不能被修改数据;
  2. 外置缓存:使用另一个Map,用于存放用户自定义数据 。默认是不开启的。Hibernate框架值提供外置缓存的规范(接口),需要第三方来实现。

1. 二级缓存结构

2. 缓存并发访问策略(Cache Concurrency Strategy)

4个Hibernate内置的并发策略:

transactional—Available in a managed environment only. It guarantees full transactional isolation up to repeatable read, if required. Use this strategy for read-mostly data where it’s critical to prevent stale data in concurrent transactions, in the rare case of an update.
read-write—Maintains read committed isolation, using a timestamping mechanism. It’s available only in non-clustered environments. Again, use this strategy for read-mostly data where it’s critical to prevent stale data in concurrent transactions, in the rare case of an update.
nonstrict-read-write—Makes no guarantee of consistency between the cache and the database. If there is a possibility of concurrent access to the same entity, you should configure a sufficiently short expiry timeout. Otherwise, you may read stale data in the cache. Use this strategy if data rarely changes(many hours, days or even a week) and a small likelihood of stale data isn’t of critical concern. Hibernate invalidates the cached element if a modified object is flushed, but this is an asynchronous operation, without any cache locking or guarantee that the retrieved data is the latest version.
read-only—A concurrency strategy suitable for data which never changes.Use it for reference data only.

摘自《Hibernate in Action》

一般来说,最经常使用的是read-only的策略,因为二级缓存的应用场景本来就是很少被修改的数据。

3. 应用场景

  1. 适合存储到二级缓存的数据:很少被修改的数据,不是很重要的数据,允许偶尔出现并发访问问题,比如国家、地区信息等长时间不变的;
  2. 不适合存储到二级缓存的数据:经常被修改的数据,绝对不允许出现并发访问问题的,比如财务数据,与其他应用共享的数据。

4. 二级缓存的提供商

Hibernate框架只是提供了二级缓存的规范(接口),有提供商来实现接口,有一下二级缓存提供商:

  1. EHCache:最常使用;

  2. OpenSymphony

  3. SwarmCache

  4. JBossCache

    这四个提供商实现的二级缓存对并发策略的支持(打叉标志是表示支持):

5. 整合二级缓存EHCache

1. 导入jar包

2. 配置 hibernate.cfg.xml

3. 配置类缓存、集合缓存

4. ehcache.xml配置文件

ehcache.xml配置文件复制到src目录下

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
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">

<!--设置临时文件的存储位置,当缓存到一定程度的时候会写到硬盘-->
<diskStore path="java.io.tmpdir"/>

<!--
maxElementsInMemory :设置基于内存的缓存中可存放的对象最大数目
eternal:设置对象是否为永久的,true表示永不过期,此时将忽略
timeToIdleSeconds 和 timeToLiveSeconds属性; 默认值是false
timeToIdleSeconds:设置对象空闲最长时间,以秒为单位, 超过这个时间,对象过期。
当对象过期时,EHCache会把它从缓存中清除。如果此值为0,表示对象可以无限期地处于空闲状态。
timeToLiveSeconds:设置对象生存最长时间,超过这个时间,对象过期。如果此值为0,
表示对象可以无限期地存在于缓存中. 该属性值必须大于或等于 timeToIdleSeconds 属性值
overflowToDisk:设置基于内在的缓存中的对象数目达到上限后,是否把溢出的对象写到基于硬盘的缓存中
diskPersistent:当jvm结束时是否持久化对象 true false 默认是false
diskExpiryThreadIntervalSeconds:指定专门用于清除过期对象的监听线程的轮询时间
memoryStoreEvictionPolicy:当内存缓存达到最大,有新的element加入的时候,
移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>

6 . 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test2() {
Session session1 = HibernateUtils.openSession();
Session session2 = HibernateUtils.openSession();

Customer customer1 = (Customer) session1.get(Customer.class, 9);
System.out.println(customer1.getName());
session1.clear();

/*如果Customer没有被放入二级缓存的类缓存区,那么当一级缓存被清空之后下面的查询还会
再次执行SQL查询,但是Customer被放到类缓存区,就可以从类缓存区取出(缓存),无需
再执行SQL查询数据库*/
System.out.println("--------------------");
Customer customer2 = (Customer) session1.get(Customer.class, 9);
System.out.println(customer2.getName());

// 关闭session1
session1.close();

// 哪怕是用另一个session查询,也会优先从二级缓存中查询是不是有这个对象
Customer customer3 = (Customer) session2.get(Customer.class, 9);
System.out.println(customer3.getName());
session2.close();
}

类级别的缓存:一级缓存存储的是PO(持久化对象的引用),也就是在同一个session中获取同一个OID指向的对象获得的是同一个对象(同一个内存地址的对象);二级缓存存储的是对象的散装数据,不同的事务要获取同一个OID指向的对象是从二级缓存中获取对象的散装数据,再组装为对象,因此获得的是不同的对象。(相当于重新new一个对象,再分别根据散装数据设置不同的属性)。

集合级别的缓存:存储的是关联的对象的OID的值,如果需要数据,就会从类级别缓存中获取,如果类级别缓存中没有数就查询数据库。

查询缓存:查询缓存不是将OID作为key,而是将HQL查询语句作为key,将查询结果的ID列表作为value(存储的只是结果的id列表)。一个HQL第一次执行时,查询数据库,将HQL作为key、结果的OID列表作为value放到查询缓存,同时将结果集存储到二级缓存。当再次执行同样的HQL时,根据查询缓存的OID列表去二级缓存(类级别的缓存)中去获取数据,封装为对象。

查询缓存的问题:

  1. 两个HQL稍微不同(比如第一个查询id在10-20之间,第二个查询id在15-20之间),那么就会被认为斯不同的key,导致缓存的利用率不高;
  2. 查询缓存必须配置二级缓存使用,如果二级缓存设置了超时时间,哪怕冲查询缓存中获得了要查询的对象的id,也会因为到二级缓存中查询时超时就必须根据获得的id一个一个去数据库查询(有多少id就查询多少次数据库),反而使得性能下降;

查询缓存配置

  1. 开启查询缓存
1
2
<!--开启查询缓存-->
<property name="hibernate.cache.use_query_cache">true</property>
  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
/**
* 查询缓存,一下的代码只执行了第一次查询的SQL语句,后面两次获取数据
* 都是在缓存中获取数据
*/
@Test
public void test2() {
Session session1 = HibernateUtils.openSession();
Query query = session1.createQuery("from Customer");

// 设置允许缓存
query.setCacheable(true);
List<Customer> customers1 = query.list();
System.out.println(customers1);

session1.close();

Session session2 = HibernateUtils.openSession();
// 再次执行一样HQL,就会从查询缓存中获取OID,再去二级缓存(类缓存)中获取数据
query = session2.createQuery("from Customer");

// 设置允许从缓存中获取数据,否则还是查询数据库
query.setCacheable(true);
List<Customer> customers2 = query.list();
System.out.println(customers2);

// 从二级缓存中获取数据,组装为对象
Customer customer = (Customer) session2.get(Customer.class, 9);
System.out.println(customer);

session2.close();
}