1.主键生成策略
1.1 主键的两种类型
- 自然主键:把数据表中的某一业务字段作为表的主键。如一张用户表中,把用户的用户名作为用户表的主键。这样做的前提条件是,1.用户的用户名不能为空,2.用户的用户名不能重复,用户的用户名不能修改。这样尽管也是可以的,但不能很好的满足业务需求的改变,所以不推荐使用自然主键的方式。
- 代理主键:单独为数据表设置一个字段作为数据表的主键。作为主键的这个字段没有业务含义,一般直接取名为id,通常为整数类型,因为整型要比字符型节省数据库的空间,所以一般都是使用代理主键的方式设置数据表的主键。
注意:在开发中,建议使用代理主键。
1.2 hibernate 中主键的生成策略
- assigned 自然主键类型
在程序中设置主键。如果在映射表中不设置generator
属性,hibernate 默认使用该主键生成策略。但不建议使用这种方式,尽量要减少手动对主键的操作。 - increment 代理主键类型
用于整型类型,由 hibernate 自动以递增的方式生成,每次增量为一,但只有当没有其他进程相同一张表中插入数据时,才可以使用,不能在集群环境下使用。 - identity 代理主键类型
由底层数据库设置主键,与 hibernate 无关。但前提是使用的数据库要支持自动增长数据类型,如 MySQL 是支持主键自动生成的,但 Oracle 就不支持主键自动生成。如果数据库支持主键自增,是可以采用该主键生成策略的。 - sequence 代理主键类型
由底层数据库根据序列生成主键,与 hibernate 无关。但前提是数据库要支持序列,Oracle 是支持的。如果数据库支持序列,是可以采用该主键生成策略的。 - hilo 代理主键类型
hibernate 生成主键,hilo 是 high low (高低位方式)的缩写,是 hibernate 常用的一种生成方式,需要一张额外的表来保存 hi(高位)的值,并手动设置 max_lo 的值,然后通过算法公式(hi * (max_lo + 1) + 0)来生成主键。这种生成策略可以跨数据库,但由hilo算法生成的标志只能保证在一个数据库是唯一的。 - natve 代理主键类型
根据底层数据库,自动选择identity、sequence、hilo 策略。但由于生成策略的控制权在 hibernate 手上,不建议采用,并且这种生成策略效率比较低。 - uuid 代理主键类型
由 hibernate 使用 128 为的UUID算法来生成标识符(主键),该算法可以在网络环境中生成唯一字符串的标识符。长度是一个 32 位的十六进制字符串,占用控空间比较大,对应数据库的char/varchar类型。这种生成策略与数据库无关,所以可以跨数据库,方便数据库移植,效率也很高,因为不访问数据库就可以生成主键值,并且可以保证唯一性。
2.持久化类
2.1 持久化类的编写规则
实体类经过 hibernate 操作转换成持久化类,下面还是使用实体类说明规则。
- 实体类提供无参的构造方法。
无参的构造方法就算是不写也可以,因为 jdk 会帮我们做,但最好加上这个无参的构造方法。 - 实体类的属性要是私有的,并使用公开的 set 和 get 方法操作
hibernate 在底层会将查询到的数据进行封装,使用反射生成类的实例。 - 实体类中要有属性作为唯一值
hibernate 要通过唯一的标识区分内存中是否有一个持久化类,在 java 中是通过地址区分是否是同一个对象的,在关系型数据库的表中是通过主键区分是否有一条记录的,在内存中,hibernate 是不允许出现两个OID (对象唯一标识符)相同的持久化类的。 - 实体类属性的基本类型建议使用基本数据类型的包装类
包装类和基本数据类型的默认值是不同的,比如 int 类型的默认值是 0,Integer 类型的默认值是 null。并且包装类的语义描述比基本数据类型更加清晰,比如,一个学生的成绩,可以是 0 分,也可以是 100 分,但如果这个学生没有成绩,用基本的数据类型就很难表示了,但包装类就可以用 null 来表示,这样不会产生歧义。 - 映射的实体类不要使用final关键字修饰
hibernate 有延迟加载机制,这个机制会产生代理对象,产生代理对象是通过字节码的增强技术来完成的,其实就是产生了当前类的子类对象实现的,而是用 final 关键字修饰就无法产生子类。
2.2 持久化类的三种状态
- 瞬时态(临时态)(自由态)
瞬时态是对象只是 new 了出来,在内存开辟了空间,但还没有和 session 关联,也即是还没有使用 session 操作内存中的对象,这时候在数据库里面是没有记录的。 - 持久态
new 出来的实体化类对象经过 session 的操作,被加入到 session 的缓存中,并且与这个对象关联的 session 也没有关闭,这个时候就是持久态,在数据库中存在对应的记录,每条记录对应唯一的持久化对象,注意持久化对象是在还未提交事务钱就已经是持久态了。 - 托管态(游离态)(离线态)
某个持久态的实例在和 session 对象关联后,session 被关闭时,这个对象就变成了托管态,这个对象属性值发生改变时,hibernate 就无法检测到,因为这个实例对象已经失去了和 session 的关联。
关于这三种状态的理解,可以结合下面的 curd 操作和一级缓存来理解。
3.curd 操作
实体类的代码
package cc.wenshixin.entity;
public class Notice {
private int id; // 公告序号
private String title; // 公告标题
private String content; // 公告内容
private String people; // 发布人
private String date; // 发布日期
public Notice()
{
}
public Notice(String title, String content, String people, String date) {
super();
this.title = title;
this.content = content;
this.people = people;
this.date = date;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getPeople() {
return people;
}
public void setPeople(String people) {
this.people = people;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
@Override
public String toString() {
return "Notice [id=" + id + ", title=" + title + ", content=" + content + ", people=" + people + ", date=" + date
+ "]";
}
}
hibernate 自定义的工具类,方便操作 hibernate。
package cc.wenshixin.utility;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class HibernateUtility {
private static Configuration cfg = null;
private static SessionFactory sessionFactory = null;
//静态代码块
static
{
//加载核心配置文件
cfg = new Configuration().configure();
sessionFactory = cfg.buildSessionFactory();
}
/*提供方法返回sessionFactory*/
public static SessionFactory getSessionFactory()
{
return sessionFactory;
}
/*提供于本地线程绑定的session方法*/
public static Session getSession()
{
return sessionFactory.getCurrentSession();
}
}
下面的操作都是使用JUnit测试工具测试的代码。
3.1 增加操作
增加操作让持久化类从瞬时态变为持久态。
@Test
public void testSave()
{
//得到sessionFactory对象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得session对象
Session session = sessionFactory.openSession();
//开启事务
Transaction tx = session.beginTransaction();
/*执行curd操作*/
Notice notice = new Notice("实验室开放", "同学们课外可自由选择实验", "admin", "2017-10-1");
session.save(notice);
//执行事务
tx.commit();
//关闭session和sessionFactory
session.close();
sessionFactory.close();
}
3.2 查询操作
hibernate 的删改操作都是基于查询操作实现的。
@Test
public void testGet()
{
//得到sessionFactory对象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session
Session session = sessionFactory.openSession();
//开启事务
Transaction tx = session.beginTransaction();
//执行查询操作
Notice notice = session.get(Notice.class, 2);
//这里要先重写toString()方法
System.out.println(notice.toString());
//提交事务
tx.commit();
//关闭session和sessionFactory
session.close();
sessionFactory.close();
}
3.3 删除操作
下面展示了两种方式来删除一条记录,但建议使用第一种,先查询后删除的方式,应该避免第二种直接设置主键对应属性值的方式。
@Test
public void testDelete()
{
//得到sessionFactory对象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session对象
Session session = sessionFactory.openSession();
//开启事务
Transaction tx = session.beginTransaction();
//执行删除操作
//第一种方法
//Notice notice = session.get(Notice.class, 3);
//第二种方法
//Notice notice = new Notice();
//notice.setId(2);
session.delete(notice);
//提交事务
tx.commit();
//关闭session和sessionFactory
session.close();
sessionFactory.close();
}
3.4 修改操作
先得到持久态的对象,再对这个对象进行操作。
@Test
public void testUpdate()
{
//得到sessionFactory对象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session对象
Session session = sessionFactory.openSession();
//开启事务
Transaction tx = session.beginTransaction();
//执行更新操作
Notice notice = session.get(Notice.class, 2);
notice.setTitle("我改变了");
session.update(notice);
//执行事务
tx.commit();
//关闭session和sessionFactory
session.close();
sessionFactory.close();
}
3.5 增加或更新操作
saveOrUpdate()方法是更具持久化对象的状态来做增加或者更新操作的,对象如果是瞬时态,那么执行事务就做增加操作,如果对象是托管态,那么执行事务就做更新操作,但此时要注意,更新操作要把持久化类的所有属性都设置值,否则没有设置属性值的字段为null,下面的代码就会产生这种情况,所以不推荐使用托管态修改数据表种的记录。
@Test
public void testSaveOrUpdate()
{
//得到sessionFactory对象
SessionFactory sessionFactory = HibernateUtility.getSessionFactory();
//得到session对象
Session session = sessionFactory.openSession();
//开启事务
Transaction tx = session.beginTransaction();
//执行增加或更新操作
//Notice notice = new Notice("新的公告", "公告内容", "admin", "2017-10-9");
Notice notice = new Notice();
notice.setId(4);
notice.setPeople("admin");
session.saveOrUpdate(notice);
//提交事务
tx.commit();
//关闭session和sessionFactory
session.close();
sessionFactory.close();
}
3.6 持久化类状态之间的转化
- 瞬时态转其他状态
瞬时态转持久态:执行 session 对象的 save()方法或者 saveOrUpdate()方法
瞬时态转托管态:为瞬时态对象设置持久化标识,也即是调用 setId()方法
Notice notice = new Notice(); //瞬时态
notice。setId(2); //托管态
- 持久态转其他状态
持久化对象可以通过 session 对象执行 get()和 load()方法,或者 Query 查询(后面会说到)从数据库种获得。
持久态转瞬时态:执行session的delete()方法
持久态转托管态:执行 session 的close()、clear() 或者 evict() 方法,evict()方法用于清除一级缓冲中的某一个对象,close()方法是用来关闭 session 对象,清除整个一级缓存,clear()方法用于清除一级缓存中的所有对象。 - 托管态转气态状态
托管态对象是无法直接得到的,是由其他状态对象转化而来的,而托管态和瞬时态的区别就是 OID 有没有值。
托管态转持久态:执行 session 的 update()、saveOrUpdate()或者lock()方法
托管态转瞬时态:将托管态的持久化的 OID标识设置为 null,也即是将作为主键的属性值设置为 null
注意:由于持久化态对象的值改变,其实不用调用 update()方法或者 saveOrUpdate()方法,在执行完事务后就可以自动更新数据库的(在一级缓存中会解释自动更新),但是还是建议把方法加上,便于阅读代码。
4.一级缓存
4.1 什么是一级缓存
首先我们要明白什么是缓存,数据库本身其实就是一个文件系统,并且我们知道使用流的方式操作文件效率不高,所以我们把数据放到内存里面,这样就可以直接读取内存里面的数据,提高读取的效率。
hibernate 框架提供了很多的优化方式,一级缓冲就是优化方式之一。hibernate 还有二级缓存,但现在已经不适用了,使用 redis技术来代替了。
hibernate 的一级缓存就是指 session 缓存,session 缓冲就是一块内存空间,用来存放相互管理的 java 对象,在使用 hibernate 查询对象时,先根据对象的 OID(唯一标识符)去一级缓存中查找,如果找到就直接从一级缓存中取出使用,不用再去数据库查询了,这样就提高了查询效率,如果一级缓存中没有,就要去数据库中查询,然后把查到的数据信息放到一级缓存中。hibernate 的一级缓存的作用就是减少对数据库的访问。
4.2 一级缓存的特点
- 1.hibernate 的一级缓存默认时打开的。
- 2.hibernate 的一级缓存使用范围就是 session 范围,是从 session 创建到 session 关闭。
- 3.hibernate 的一级缓存,存储数据必须是持久化数据。
4.3 验证一级缓存的存在
Notice notice1 = session.get(Notice.class, 1);
System.out.println(notice1);
Notice notice2 = session.get(Notice.class, 1);
System.out.println(notice2);
//比较的是对象的指向的地址是否一样
System.out.println(notice1==notice2);
连续执行查询操作,观察控制台的输出,发现只出现了一次查询的 sql 语句,这就说明第二次的查询不是在数据库中查询得到的,而是直接从 hibernate 的一级缓存中取的,并且比较两个对象引用的地址也是true。
4.4 解释持久化类自动更新
在前面我们说持久化类改变属性值后,不需使用 update()方法就可以自动更新数据库里面的记录,我们需要指导 hibernate 一级缓存的内部结构。在执行完查询操作后,把查询到的数据放到缓冲区,并且复制一份数据到快照区,直接通过 set 方法改变持久化对象的属性值,也会改变缓冲区里面的内容,在提交事务时比较缓冲区和快照区里面的数据是否一致,如果不一致,就更新数据库中的记录,并更新快照区中的数据。快照区的作用就是确保一级缓存中的数据和数据库中的数据一致。
5.事务操作
hibernate 是 jdbc 的轻量级封装,hibernate 的事务处理就是数据库的事务处理。
5.1 什么是事务
在数据库操作上,一项事务是由一条或多条操作数据库的 sql 语句组成的一个不可分割的工作单元。只有当事务中的所有操作都正常完成,整个事务才会被提交到数据库中。如果事务中由一项操作没有完成,则整个事务就会被回滚。事务简单理解起来就是,一组逻辑上的操作,组成这组操作的各个单元,要么一起成功,要么一起失败,具有统一性。
5.2 事务的四个特性详解
事务有很严格的定义,需要同时满足下面的四个特性,这四个特性通常称之为 ACID 特性。
- 原子型(Atomic):表示将事务中所做的操作捆绑成一个不可分割的单元,即对事务所进行的数据修改等操作,要么全部执行,要么全都不执行。
- 一致性(Consistency):表示事务完成时,必须使所有的数据都保持一致状态。
- 隔离性(Isolation):指一个事务的执行不能被其他事务干扰,即一个事务内部的操作以及使用的数据对并发的其他事务都是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(Durability):持久性也称永久性,指一个事务一旦被提交,它对数据库中的数据改变就应该是永久性的。提交后其他事务对其他操作或故障不会对它有任何影响。
5.3 事务的并发问题
在实际应用中,数据库是要被多个用户共同访问的,在多个事务同时使用相同的数据时,可能会发生并发的问题。
- 脏读:一个事务读取到了另一个事务未提交的数据。
- 不可重复度:一个事务读到了另一个事务已经提交的 update 的数据,导致在同一个事务中的查询结果不一致。
- 虚读/幻读:一个事务读到了另一个事务已经提交的 insert 的数据,导致在同一个事务中的多次查询结果不一致。
5.4 事务的隔离级别
为了避免上面所说的事务并发问题发生,所以在标准的 SQL 规范中,定义了四个事务隔离级别,不同的隔离级别对事务的处理是不同的。
- 读未提交(Read Uncommitted, 1级):一个事务在执行过程中,即可以访问其事务未提交的新插入的数据,又可以访问未提交的修改数据。如果一个事务已经开始写数据,而另一个事务则不允许同时进行写操作,但允许其他事务读此行数据,此隔离级别可防止丢失更新。
- 已提交读(Read Commited,2级):一个事务在执行过程中,既可以访问其他事务成功提交的新插入的数据,又可以访问成功修改的数据。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。此隔离级别可有效防止脏读。
- 可重复读(Repeated Read,4级):一个事务在执行过程中,可以访问其他事务成功提交的新插入的数据,但不可以访问成功修改的数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务,此隔离级别可有效的防止不可重复读和脏读。
- 序列化/串行化(Serializable,8级):提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。此隔离级别可有效的防止脏读,不可重复读和幻读。
事务的隔离级别是由数据库提供的,但并不是所有数据库都支持四种隔离级别的。在使用数据库时,隔离级别越高,安全性越高,性能越低。在实际的开发中,不会选择最高或者最低的隔离级别,使用数据库默认的即可。
5.5 hibernate 事务规范代码
在 hibernate 中,可以通过代码来操作管理事务,如通过 Transaction tx = session.beginTransaction();
开启一个事务,持久化操作后,通过 tx.commit();
提交事务,如果事务出现异常,要通过 tx.rollback();
操作来撤销事务(回滚事务)。
除了在代码中对事务开启,提交和回滚操作外,还可以在 hibernate 的配置文件中对事务进行配置。在配置文件中,可以设置事务的隔离级别。其具体的配置方法是在 hibernate.cfg.xml 文件中的 property
标签中进行的。配置方法:<property name="hibernate.connection.isolation">4</property>
,并且我们在进行正真的事务管理时,需要考虑到事务的应用场景,事务的控制不应该放在 DAO 层,而应该放在 Service 层调用多个 DAO 实现一个业务逻辑的操作。其实最主要的是如何保证在 Service 中开启事务时使用的 Session 对象和 DAO 中多个操作使用的是同一个 Session 对象。
下面有两种解决办法。
- 可以在业务层获取到 Session,并将 Session 作为参数传递给 DAO。
- 可以使用 ThreadLocal 将业务层获取的 Session 绑定到当前线程,然后在 DAO 中获取 Session 时都从当前的线程中获取。
第二种方式时最优的方案,而且具体的实现,hibernate 已经在内部完成了,我们只需要配置一下。hibernate5 种提供了三种管理 Session 对象的方法。
- Session 对象的生命周期与本地线程绑定
- Session 对象的生命周期与 JTA(Java Transaction API,Java事务API,是一个Java企业版的应用程序接口)事务绑定
- hibernate 委托程序管理 Session 对象的生命周期
在 hibernate 的配置文件中,hibernate.current_session_context_class 属性用于指定 Session 管理方式,可选值有:1. tread,Session 对象的生命周期与本地线程绑定;2. jta,Session 对象的生命周期与 JTA 事务绑定;managed,hibernate 委托程序来管理 Session 对象的生命周期。在这里我们选择 tread 值,在 hibernate.cfg.xml 中进行配置:<property name="hibernate.current_session_context_class">thread</property>
,并且在 hibernate 中提供了 getCurrentSession()方法来创建一个 Session 和本地线程 TreadLocal 绑定的方法。
/*提供于本地线程绑定的session方法*/
public static Session getSession()
{
return sessionFactory.getCurrentSession();
}
hibernate 提供的这个与本地线程绑定的 Session 可以不用关闭,当线程执行结束后,就会自动关闭了。
下面给出事务操作的规范代码写法。
代码结构如下:
try {
开启事务
提交事务
}catch() {
回滚事务
}finally {
关闭
}
@Test
public void testTx1()
{
Session session = null;
Transaction tx = null;
try{
//得到与本地线程绑定的 Session
session = HibernateUtility.getSession();
//开启事务
tx = session.beginTransaction();
//添加操作
Notice notice = new Notice("本地线程绑定", "规范操作", "admin", "2017-10-8");
session.save(notice);
//提交事务
tx.commit();
} catch (Exception e) {
e.printStackTrace();
//回滚事务
tx.rollback();
} finally {
//不需要关闭session
}
}
//下面的代码只是上面代码的对比
@Test
public void testTx2()
{
SessionFactory sessionFactory = null;
Session session = null;
Transaction tx = null;
try{
sessionFactory = HibernateUtility.getSessionFactory();
//不是与本地线程绑定的 Session,类似于单例模式。
session = sessionFactory.openSession();
//开启事务
tx = session.beginTransaction();
//添加操作
Notice notice = new Notice("本地线程绑定", "规范操作", "admin", "2017-10-8");
session.save(notice);
//提交事务
tx.commit();
} catch (Exception e) {
e.printStackTrace();
//回滚事务
tx.rollback();
} finally {
//需要关闭session
session.close();
sessionFactory.close();
}
}
6.hibernate 查询相关API的简单介绍
在前面,我们只进行了简单的 curd 操作,对于查询操作,hibernate 还有几种不同的 API 可以选择使用,在这里先简单介绍一下,在后面还会详细叙述。
6.1 Query 对象
使用 query 对象,不需要写 sql 语句,但要写简单的 hql(hibernate query language,hibernate 的查询语言) 语句。
hql 和 sql 语句的区别:
- hql 语句是直接使用实体类和属性来做查询
- sql 语句是要操作数据表和字段
hql语句的写法:from 实体类的名称
。
Query 对象的使用:
- 创建 query 对象
- 调用 query 对象里面的方法得到结果
示例代码如下:
@Test
//查询表中所有数据
public void testQuery1()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Query<Notice> query = session.createQuery("from Notice");
List<Notice> list = query.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
@Test
//有条件的查询
public void testQuery2()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Query<Notice> query = session.createQuery("from Notice where title=?");
query.setString(0, "实验室开放");
List<Notice> list = query.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
6.2 Criteria 对象
使用 criteria 对象,不需要写语句,直接调用方法来实现。
criteria 对象的使用:
- 创建 criteria 对象
- 调用对象里面的方法得到结果
示例代码如下:
@Test
//查询表中所有数据
public void testCriteria1()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Criteria criteria = session.createCriteria(Notice.class);
List<Notice> list = criteria.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
@Test
//有条件的查询
public void testCriterial2()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
Criteria criteria = session.createCriteria(Notice.class);
criteria.add(Restrictions.eq("title", "实验室开放"));
List<Notice> list = criteria.list();
for(Notice notice : list)
{
System.out.println(notice);
}
}
6.3 SQLQuery 对象
从名字就可以看出是和 sql 有关的,直接写 sql 语句,底层 hibernate 调用的是 sql 语句实现的。
SQLQuery 对象
- 创建 SQLQuery 对象
- 调用对象的方法得到结果
示例代码如下:
@Test
public void testSQLQuery()
{
Session session = HibernateUtility.getSession();
Transaction tx = session.beginTransaction();
SQLQuery sqlQuery = session.createSQLQuery("SELECT * FROM notice_content");
List<Object[]> list = sqlQuery.list();
for(Object[] objects : list)
{
System.out.println(Arrays.toString(objects));
}
}