一、持久化类
1.1 什么是持久化类?
Hibernate是持久层的ORM映射框架,专注于数据的持久化工作。所谓的持久化,就是将内存中的数据永久存储到关系型数据库中。那么知道了什么是持久化,什么又是持久化类呢?其实所谓的持久化类指的是一个Java类与数据库表建立了映射关系,那么这个类称为是持久化类。其实你可以简单的理解为持久化类就是一个Java类有了一个映射文件与数据库的表建立了关系。那么我们编写持久化类的时候有哪些要求呢?
1.2 持久化类的编写规则
我们在编写持久化类的时候需要有一下几点需要注意:
- 持久化类需要提供无参数的构造方法。因为在Hibernate的底层需要使用反射生成类的实例。
- 持久化类的属性需要私有,对私有的属性提供公有的get和set方法。因为在Hibernate底层会将查询到的数据进行封装。
- 持久化类的属性要尽量使用包装的类型。因为包装类和基本数据类型的默认值不同,包装类的类型语义描述更清晰而基本数据类型不容易描述。举个例子:
假设表中有一列员工工资,如果使用double类型,如果这个员工工资忘记录入到系统中,系统会将默认值0存入到数据库,如果这个员工的工资被扣完了,也会向系统中存入0.那么这个0就有了多重含义,而如果使用包装类类型就会避免以上情况。如果使用Double类型,忘记录入的工资就会存入null,而如果这个员工的工资被扣完了,就会存入0,不会产生歧义。
- 持久化类要有一个唯一标识OID与表的主键对应。因为Hibernate中需要通过这个唯一标识OID区分在内存中是否是同一个持久化类。在Java中通过地址区分是否是同一个对象,在关系型数据库的表中是通过主键区分是否是同一条记录。那么Hibernate就是通过这个OID来进行区分的。Hibernate是不允许在内存中出现两个OID相同的持久化对象的。
- 持久化类尽量不要使用final进行修饰。因为Hibernate中有延迟加载的机制,这个机制中会产生代理对象,Hibernate产生代理对象使用的是字节码的增强技术完成的,其实就是产生了当前类的一个子类对象实现的。如果使用了final修饰持久化类。那么就不能产生子类,从而就不会产生代理对象,那么Hibernate的延迟加载策略(是一种优化手段)就会失效。
持久化类我们已经可以正常编写了,但是在持久化类中需要有一个唯一标识OID与表的主键去建立映射关系。而且主键一般我们是不会让客户手动录入的,一般我们是由程序生成主键。那么Hibernate中也提供了相应的主键生成的方式,下面我们来看Hibernate的主键生成策略。
二、主键生成策略
2.1 主键的类型
在讲解Hibernate的主键生成策略之前,先来了解两个概念,即自然主键和代理主键,具体如下:
- 自然主键(少见):把具体业务含义的字段作为主键,称之为自然主键。例如在customer表中,如果把name字段作为主键,其前提条件必须是:每一个客户的名字不允许为null,不允许客户重名,并且不允许修改客户姓名。尽管这也是可行的,但是不能满足不断变化的业务需求,一旦出现了允许客户重名的业务需求,就必须修改数据模型,重新定义表的主键,这给数据库的维护增加了难度。
- 代理主键:把不具备业务含义的字段作为主键,称之为代理主键。该字段一般取名为”ID“,通常为整数类型,因为整数类型比字符串类型要节省更多的数据库空间。在上面的例子中,显然更合理的方式是使用代理主键。
2.2 主键生成策略
Hibernate中,提供了几个内置的主键生成策略,其常用主键生成策略的名称和描述如下:
- 自然主键
assigned:自然主键生成策略. hibernate不会管理主键值.由开发人员自己录入。如果不知道id元素的generator属性,则默认使用该主键生成策略。
- 代理主键
identity:主键自增.要求在数据库中把主键定义为自增长类型.录入时不需要指定主键.
sequence:Oracle中的主键生成策略.
increment(了解):主键自增.由hibernate来维护.每次插入前会先查询表中id最大值.+1作为新主键值.只有当没有其他进程向同一张表中插入数据时才可以使用,不能在集群环境下使用。
native:hilo+sequence+identity 自动三选一策略.适合跨数据库平台开发
uuid:产生随机字符串作为主键. 主键类型必须为string 类型.
三、持久化对象的状态
3.1 持久化对象三种状态的概述
Hibernate为了更好的来管理持久化类,将持久化类分成了三种状态:瞬时态、持久态和脱管态,一个持久化类的实例可能处于三种不同状态中的某一中。
1、瞬时态(transient)
瞬时态也称为临时态或者*态,瞬时态的实例是由new命令创建、开辟内存空间的对象,不存在持久化标识OID(相当于主键值),尚未与Hibernate的Session关联,在数据库中也没有记录,失去引用后将被JVM回收。瞬时状态的对象在内存中是孤立存在的,与数据库的数据没有任何关联,仅是一个信息携带的载体。
2、持久态(persistent)
持久态的对象存在持久化标识OID,加入到了Session缓存中,并且相关联的Session没有关闭,在数据库中有对应的记录,每条记录只对应唯一的持久化对象,需要注意的是,持久态对象是在事务还未提交前变成持久态的。
3、脱管态(detached)
脱管态也称为离线态或者游离态,当某个持久化状态的实例与Session的关联被关闭时就变成了脱管态。脱管态对象存在持久化标识OID,并且仍然与数据库中的数据存在关联,只是失去了与当前Session的关联,脱管状态对象发生改变时Hibernate不能检测到。
3.2 区分对象的三种状态
@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对象就变成了脱管态。
3.3 三种状态的转换
从图中可以看出,当一个对象被执行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,使其变成脱管态对象。
Customer customer = new Customer(); // 瞬时态
customer.setCust_id(1); //脱管态
【持久态对象转换到其他状态】
持久化对象可以直接通过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对象将由脱管态转化为瞬时态。
3.4 持久态对象能够自动更新数据库
// 测试持久化类的持久化对象有自动更新数据库的能力
@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的一级缓存。
四、Hibernate的一级缓存
缓存是计算机领域非常通用的概念。它介于应用程序和永久性数据存储源(如硬盘上的文件或者数据库)之间,其作用是降低应用程序直接读写永久性数据存储源的频率,从而提高应用的运行性能。缓存中的数据是数据存储源中数据的拷贝。缓存的物理介质通常是内存。
Hibernate的缓存分为一级缓存和二级缓存,Hibernate的这两级缓存都位于持久化层,存储的都是数据库数据的备份。其中第一级缓存为Hibernate的内置缓存,不能被卸载。接下来围绕Hibernate的一级缓存进行详细的讲解。
4.1 什么是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缓存会被清空。
4.2 测试一级缓存
// 证明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对象是从一级缓存中获取的。
接下来验证一下代码的执行结果是否和描述的一致。在Customer customer1 = session.get(Customer.class, 1l);这一行设置断点,用debug方式执行该方法,程序进入断点后点击单步跳过(F6),代码执行过System.out.println(customer1);语句后,控制台的输出结果如下:
从上图的输出结果中可以看出,第一次执行session.get()方法后,Hibernate向数据库发送了一条select语句,这说明此时customer1对象是从数据库中查找的。执行输出语句数据customer1对象中的数据后,继续执行程序,当执行到输出customer2对象的代码处时,控制台的输出结果如下:
从上图的输出结果可以看出,customer2对象的查询结果被直接打印了,说明第二次调用Session对象的get()方法时,没有向数据库发送select语句,而是直接从一级缓存中获取customer2对象。
之前我们介绍过,Hibernate的持久态对象能够自动更新数据库,其实就是依赖了一级缓存。那么一级缓存为什么就可以去更新数据库呢?其实是因为一级缓存的一块特殊区域——快照区。
4.3 一级缓存的内部结构(快照区)
HIbernate向一级缓存放入数据时,同时复制一份数据放入到Hibernate的快照中,当使用commit()方法提交事务时,同时会清理Session的一级缓存,这时会使用OID判断一级缓存中的对象和快照中的对象是否一致,如果这两个对象中的属性发送变化,则执行update语句,将缓存中的内容同步到数据库,并更新快照;如果一致,则不执行update语句。Hibernate快照的作用就是确保一级缓存中的数据和数据库中的数据一致。
// 一级缓存中的快照区:持久态对象能够自动更新数据库
@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();
}