前言
Hibernate是一款全自动的ORM映射框架,虽然近年来Mybatis-plus在国内使用人数较多,但是放眼全球Hibernate依旧在市场上占有一席之地。但是国内针对Hibernate高性能使用的文章较少,今天我决定填补这块儿空缺,一文带你深入了解Hibernate高性能用法。
Hibernate的持久化类的编写规则
Hibernate是持久层的ORM映射框架,专注于数据的持久化工作,也就是将内存的数据永久存储到关系型数据库中。持久化类指的是一个Java类与数据库建立了映射关系,那么这个类就是持久化类。我们在编写持久化类的时候需要注意以下几点:
- 持久化类需要提供无参数的构造方法。因为在Hibernate的底层需要使用反射生成类的实例。
- 持久化类的属性需要私有化,对私有的属性提供共有的Getter\Setter方法。因为在Hibernate底层会将查询到的数据进行封装。
- 持久化类的属性要尽量使用包装类的类型。因为包装类和基本数据类型的默认值不同,包装类的类型语义描述更清晰。例如:有一个表的某一列对应的是员工的工资,如果是double类型,且这个员工的工资忘记记录到数据库中,系统会将默认值0存入数据库,但是如果这个员工的工资被扣完,也会向系统存入0,那么这个0就有了多重含义,而包装类Double的默认值为Null就不存在上述的情况。
- 持久化类要有一个唯一的OID与表的主键对应。因为Hibernate中需要通过这个唯一标识OID区分在内存中是否是同一个持久类。Hibernate是不允许内存中出现两个OID相同的持久化对象的。
- 持久化类尽量不用final关键字修饰。因为Hibernate中有延迟加载机制,这个机制中会产生代理对象,Hibernate产生的代理对象使用的是字节码的增强技术完成的,其实就是产生了当前类的一个子类对象实现的。如果使用了final关键字就无法产生子类,Hibernate的延迟加载策略就会失效。
Hibernate的主键生成策略
在讲解Hibernate的主键生成策略之前,我们先来讲讲自然主键和代理主键两个概念,具体如下:
- 自然主键:把具有业务逻辑的字段作为主键,称之为自然主键。例如现在有一个customer表,表中有一列为name表示顾客的姓名,如果想把name字段作为主键,其前提条件是:这一列的值不能为null,不能重复且不能进行二次修改。
- 代理主键:把不具备业务含义的字段作为主键,称之为代理主键。 该字段一般取名为“ID”,通常为整数类型,因为整数类型可以节省更多的数据库空间。比如还是刚刚那个customer表,name字段可能会出现重复的情况(同名同姓的现象时有发生),这就不满足创建自然主键的前提,此时就可以用代理主键:"ID"字段。
名称 | 描述 |
---|---|
increment | 用于long、short和int类型,由Hibernate自动以递增的方式生成唯一标识符,每次增量为1。只有当没有其他进程向同一张表插入数据时才可以使用。适用于代理主键。 |
identity | 采用底层数据库本身提供的主键生产标识符,条件是数据库支持的自动增长。在DB2、MySQL等数据库中可以使用该生成器,该生成器要求在数据库中主键定义成为自增长类型。适用代理主键。 |
sequence | Hibernate根据底层数据库生成标识符。条件是数据库支持序列。适用于代理主键。 |
native | 根据底层数据库对自动生成的标识符的能力来选择identity、sequence、hilo三种生成器中的一种,适合跨数据库平台开发。适用于代理主键。 |
uuid | Hibernate采用128位的UUID算法来生产唯一标识符,其UUID被编码为一个长度为32位的十六进制字符串。但这种策略并不流行,适用于主键代理。 |
assigned | 由Java程序负责生成标识符,如果不指定id元素的generator属性,则默认使用该主键生成策略。适用于自然主键。 |
Hibernate的持久化对象的三种状态
Hibernate的持久化的对象可以划分为三类:瞬时态、持久态、脱管态,一个持久化类的实例可能处于三种不同状态的某一种,三种状态的介绍如下:
-
瞬时态:顺时态也叫临时态或者自由态,瞬时态的实例是由new命令创建、开辟内存空间的对象,不存在持久化OID(相当于主键值),尚未与Hibernate Session关联,在数据库中也没有记录,失去引用后会被JVM回收。瞬时态的对象在内存中是孤立存在的,与数据库中的数据无任何关联,仅仅是一个信息携带的载体。
-
持久态:持久态的对象存在持久化标识OID,加入到了Session缓存中,并且相关联的Session没有关闭,在数据库中有对应的记录。每条记录只对应唯一的持久化对象,需要注意的是,持久态的对象是事务还未提交前变成持久态的。
-
脱管态:脱管态也称游离态或者离线态,当某个持久态的实例与Session的关联被关闭时就变成了脱管态。脱管态对象存在持久化OID,并且仍然与数据库中的数据存在关联,只是失去了与当前Session的关联,脱管态对象发生改变的时候Hibernate不能被检测到。
public void test(){
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();
Customer custmer = new Customer();// 瞬时态对象:没有持久化标识OID,没有被session管理。
customer.setCust_name("张三");
Serializable id = session.save(customer);// 持久化对象:有持久化标识OID,被session管理。
tx.commit();
session.close();
System.out.println(customer);// 脱管态对象:有持久化OID,没有被session管理。
}
现在已经了解了Hibernate持久化对象的三种状态,这三种状态可以通过一系列方法进行转换:
持久化对象的三种状态可以通过调用Session中的一系列方法实现状态间的转换,具体如下:
- 瞬时态转为其他状态:
-
瞬时态转为持久态:执行Session的save()或saveOrUpdate()方法。
-
瞬时态转为脱管态:为瞬时态设置持久化标识OID。
Customer customer = new Customer(); //瞬时态 customer.setCust_id(1); //脱管态
- 持久态转为其他状态:
- 持久态转为瞬时态:执行Session的delete()方法,需要注意被删除的是持久化对象。
- 持久态转为脱管态:执行Session的close()、clear()方法。close()方法用于关闭Session,清除一级缓存;clear()方法用于清除一级缓存的所有对象。
- 脱管态转为其他状态:
- 脱管态转为持久态:执行Session的Update()、saveOrUpdate()或lock()方法。
- 脱管态转为瞬时态:将脱管态的持久化表示OID设置为Null。
持久态有一个非常重要的特性:持久态对象能自动更新数据库。
public void test(){
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();
//获得持久态的对象
Customer customer = seesion.get(Customer.class,11);
customer.setCust_name("王五");
//session.update(customer); 不用调用该方法也能更新成功
tx.commit();
session.close();
}
执行测试我们会发现,我们并没有执行update()方法,但是Hibernate就已经完成了更新。之所有有这样的能力是依赖于Hibernate底层的一级缓存。
Hiberntae的一级缓存
Hibernate的缓存分为一级缓存和二级缓存,这两级缓存都位于持久化层,存储的都是数据库数据的备份。其中第一级缓存为Hibernate的内置缓存,不能被卸载。接下来是围绕Hibernate的一级缓存进行详细的讲解。
Hibernate的一级缓存就是指Session缓存,Session缓存就是一块内存空间,用来存放相互管理的Java对象,在使用Hibernate查询的时候,首先会使用对象属性的OID值在Hibernate的一级缓存中进行查找,如果找到匹配的OID值的对象,那么就会直接去数据库中查找对应的数据。当从数据库中查询到所需要的数据时,该数据信息也会放置到一级缓存中。Hibernate的一级缓存的作用就是减少对数据库的访问次数。
Hibernate一级缓存的特点如下:
- 当应用程序调用Session接口的save()、update()、saveOrUpdate()方法时,如果Session缓存中没对应的对象,Hibernate就会自动把从数据库中查询到的对应对象信息加入到一级缓存中。
- 当调用Session接口的load()、get()方法,以及Query()接口的list()、iterator()方法时,还会判断缓存中是否存在该对象,有则返回,不会查询数据库,如果缓存中没有要查询的对象,再去数据库中查询对应的对象,并添加到一级缓存中。
- 到调用Session的close()方法时,Session缓存会被清空。
Hibernate向一级缓存放入数据时,会同时复制一份数据放入Hibernate快照中,当使用commit()方法提交事务时同时会清理Session的一级缓存,这时会使用OID判断一级缓存中的对象和快照中的对象是否一致,如果两个对象的属性发生变化,则执行update()语句,将缓存的内容同步到数据库,并更新快照;如果一致,则不执行update()语句。Hibernate快照的作用就是确保一级缓存中的数据和数据库中的数据一致。
public void test(){
Session session = HibernateUtils.openSession();
Transaction tx = session.beginTransaction();
Customer customer1 = session.get(Customer.class,1);// 马上发送一条sql语句查询1号用户,并放入一级缓存。
System.out.println(customer1);
Customer customer2 = session.get(Customer.class,1);// 没有发生SQL语句的执行,直接从一级缓存中获取数据。
System.out.println(customer2);
System.out.println(customer1 == customer2);// true 一级缓存的是对象的地址
tx,commit();
session.close();
}
Hibernate的事务控制
Hibernate是对JDBC的轻量级封装,其主要功能是操作数据库。在操作数据库的过程中,经常会遇到事务处理的问题,那么接下来就来介绍Hibernate的事务管理。
首先我们来回顾复习一下什么是事务:在数据库操作中,一项事务是由一条或者多条SQL语句组成的一个不可分割的工作单元。当事务中的所有操作都正常完成时,整个事务才能被提交到数据库中,如果有一项没有完成,则整个事务都会被回滚。
事务有四大特性:原子性、一致性、隔离性、持久性。这四个特性通常被成为ACID特性,具体如下:
- 原子性:表示将事务所做的操作捆绑成一个不可分割的单元,即对事务所进行的数据修改等操作,要么全部执行要么全部不执行。
- 一致性:表示事务完成时,必须使所有的数据都保持一致的状态。
- 隔离性:指一个事务的执行不能被其他事务干扰。即一个事务的内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的事务之间不能相互干扰。
- 持久性:指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。提交后的其他操作或故障都不会对其有任何影响。
在实际的应用过程中,数据库是要被多个用户所共同访问的。在多个事务同时使用相同的数据时,可能会发生并发问题:
- 脏读:一个事物读取到另外一个事物未提交的数据。
- 不可重复读:一个事物读取到了另外一个事物已经提交的update数据,导致在同一个事物中的多次查询结果不一致。
- 幻读:一个事物读到了另一个事物已经提交的insert的数据,导致在同一个事物中的多次查询结果不一致。
为了避免事务并发问题的发生,在标准SQL规范中,定义了4个事务的隔离级别,不同的隔离级别对事务的处理也不同:
- 读未提交(Read Uncommitted):一个事物在执行过程中,即可以访问其他事物未提交的新插入的数据,又可以访问未提交的修改数据。如果一个事物开始写数据,则另外一个事物则不允许同时进行写操作,但允许其他事物读这行数据。此隔离级别可防止丢失更新。
- 读已提交(Read Committed):一个事物在执行过程中,既可以访问其他事物成功提交的新插入的数据,又可以访问新插入的数据。读取数据的事物允许其他事物继续访问该行数据,但是未提交的写事物将会禁止其他事物访问该行。此隔离级别可以防止脏读。
- 可重复读(Repeatable Read):一个事物在执行过程中,可以访问其他事物成功提交的新插入的数据,但是不能访问成功修改的数据。读取数据的事物将会禁止写事物(但允许读事物),写事物则禁止其他任何事物。此隔离级别可有效防止不可重复读和脏读
- 串行化(Serializable):提供严格的事物隔离。事物只能一个接一个地执行,不能并发执行。此隔离级别可以有效地防止脏读、不可重复读和幻读。
在Hibernate中,我们不仅可以通过代码来操作管理事物,还可以在配置文件中设置事物的隔离级别。具体的做法是在配置文件中“session-factory”标签元素中进行的。
<!- hibernate.connection.ioslation = 4
1 - Read Uncommitted ioslation
2 - Read Committed ioslation
4 - Repeatable Read ioslation
8 - Serializable ioslation -->
<property name="hibernate.connection.ioslation">4</property>
Hibernate的高级API
Query
Query代表面向对象的一个Hibernate查询操作。在Hibernate中,通常使用session.createQuery()方法接受一个HQL语句,然后调用Query的list()或者uniqueResult()方法执行查询。所谓的HQL是Hibernate Query Language缩写,它的语法跟SQL很像,但是是完全面向对象的。在Hibernate中使用Query对象的步骤,具体如下:
- 获得Hibernate的Session对象
- 编写HQL语句
- 调用session.createQuery()创建查询对象。
- 如果HQL语句包含参数,则调用Query的setXxx设置参数。
- 调用Query对象的list()或uniqueResult()方法执行查询。
// 1. 查询所有记录
Query query = session.createQuert("from Customer");
List<Customer> list = query.list();
// 2. 条件查询
Query query = session.createQuery("from Customer where name = :aaa and age= :bbb");
query.setString("aaa","张三");
query.setInteger("bbb",38);
List<Customer> list = query.list();
// 3. 分页查询
Query query = session.createQuery("from Customer");
query.setFirstResult(0);
query.setMaxResults(3);
List<Customer> list = query.list();
程序通过使用Query接口,将customer表中的三条数据全部输出。更详细的HQL使用,会在后续讲解,此处只需要了解Hibernate中是如何使用Query接口进行数据查询的即可。Query的常用方法如下:
- setter方法:Query接口中提供了一系列的setter方法用于设置查询语句的参数,针对不同的数据类型需要不用的setter方法。
- iterator()方法:该方法用于查询语句,返回结果是一个Iterator对象,在读取时只能按照顺序读取,它仅把使用到的数据转换为Java实体对象。
- uniqueResult()方法:该方法用于返回唯一的结果,在确保只有一条记录的查询时可以使用该方法。
- excuteUpdate()方法:该方法为Hibernate3的新特性,它支持HQL语句的更新和删除操作。
- setFirstResult()方法:该方法可以设置获取第一个记录的位置,也就是它表示从第几条记录开始查询,默认从0开始计算。
- setMaxResult()方法:该方法用于设置结果集的最大记录数,通常与setFirstResult()方法结合使用,用于限制结果集的范围,以实现分页功能。
Criteria
Criteria是一个完全面向对象,可扩展的条件查询API,通过它完全不需要考虑数据库底层如何实现,以及SQL语句如何编写,它是Hibernate框架的核心查询对象。Criteria查询,又称QBC查询,它是Hibernate的另一种对象检索方法。通常使用Criteria对象查询数据的主要步骤,具体如下:
- 获得Hibernate的Session对象。
- 通过Session获得Criteria对象。
- 使用Restrictions的静态方法创建Criterion条件对象。Restrictions类中提供了一系列用于设定查询条件的静态方法,这些静态方法都返回Criterion实例,每个Criterion实例代表一个查询对象。
- 向Criteria对象中添加Criterion条件查询。Criteria的add()方法用于加入查询条件。
- 执行Cirteria对象的list()或uniqueResult()获得结果。
// 1, 查询所有的记录
Criteria criteria = session.createCriteria(Customer.class);
List<Customer> list = criteria.list();
// 2. 条件查询
Criteria criteria = session.createCriteria(Customer.class);
criteria.add(Restrictions.eq("name","张三"));
List<Customer> list = criteria.list();
// 3, 分页查询
Criteria criteria = session.createCriteria(Customer.class);
criteria.setFirst(0);
criteria.setMaxResults(3);
List<Customer> list = criteria.list();
SQLQuery
SQLQuery就比较简单了,这个接口就是用于接受一个SQL语句进行查询,SQL语句不会封装到实体对象中,需要我们手动写代码才可封装到实体类中。
SQLQuery sqlQuery = session.createSQLQuery("select * from cst_customer");
// 封装到对象中
sqlQuery.addEntity(Cusomer.class);
List<Customer> list = sqlQuery.list();
Hibernate的检索方式
在实际开发项目时,对数据进行最多的操作就是查询,数据的查询在所有的ORM框架中都占有极其重要的地位,在Hibernate中一共有五种检索方法:对象图导航检索、OID检索、HQL检索、QBC检索、本地SQL检索,接下来我会逐一介绍这些方法。
-
对象图导航检索
对象图导航检索方式是根据已加载的对象导航到他的关联对象。它利用类与类之间的关系来检索对象。譬如要查找一个联系人对应的客户,就可以由联系人对象自动导航找到联系人所属的客户对象。当然前提必须是在对象关系映射文件上配置了多对一的关系。
LinkMan linkMan = (LinkMan)session.get(LinkMan.class,1); Customer customer = linkMan.getCusomer();
-
OID检索
OID检索方式主要指用Session的get()和load()方法加载某条记录对应的对象。
Customer customer = session.get(Customer.class,1) Customer customer = session.load(Customer.class,1);
-
HQL检索
HQL是面向对象的查询语言,它和SQL查询语言类似,但它使用的是类、对象和属性的概念而没有表和字段的概念。**在Hibernate提供的各种查询检索方式中,HQL是官方推荐的查询语言,也是使用最广泛的一种语言。**它具有如下功能:
- 在查询语句中设定各种查询条件。
- 支持投影查询、即仅检索出对象的部分属性。
- 支持分页查询。
- 支持分组查询,允许使用group by 和 having关键字。
- 提供内置聚集函数,如sum()、min()和max()。
- 能够调用用户定义的SQL函数。
- 支持自查询、即嵌套查询。
- 支持动态绑定参数。
//查询所有 Query query = session,createQuery("from Customer"); List<Customer> list = query.list(); //模糊查询 Query query = session,createQuery("from Customer c where c.custName like ?"); query,setParameter(0,"%浪%"); //排序查询 Query query = session,createQuery("from Customer order by cid desc"); List<Customer> list = query.list(); //分页查询 Query query = session.createQuery("from Customer"); query.setFirstResult(0); query.setMaxResults(3); List<Customer> list = query.list(); //聚集函数 Query query = session,createQuery("select count(*) from Customer"); Object obj = query,uniqueResult(); Long lobj = (Long)obj; int count = lobj.intValue();
-
QBC检索
QBC是Hibernate提供的另一种检索对象的方式,它只要由Crieria接口、Criterion接口和Expression类组成。
//查询所有的记录 Criteria criteria = session.createCriteria(Customer.class); List<Customer> list = criteria.list(); //模糊查询 Criteria criteria = session.createCriteria(Customer.class); criteria.add(Restrictions.like("name","%张%")); List<Customer> list = criteria.list(); //分页查询 Criteria criteria = session.createCriteria(Customer.class); criteria.setFirst(0); criteria.setMaxResults(3); List<Customer> list = criteria.list(); //排序查询 Criteria criteria = session.createCriteria(Customer.class); criteria.addOrder(Order,desc("cid")); //统计查询 Criteria criteria = session.createCriteria(Customer.class); criteria.setProjection(Projections.rowCount()); Object obj = query,uniqueResult(); Long lobj = (Long)obj; int count = lobj.intValue();
-
本地SQL检索
采用HQL或者QBC检索方式时,Hibernate生成的标准的SQL查询语句,适用于所有的数据库平台,因此这两种检索方法都是跨平台的。但是在这种情况下,可以利用Hibernate提供的SQL检索方式。
Hibernate的多表查询
在介绍Hibernate的多表查询之前,我们先来回顾一下SQL中的多表联合查询。
- 连接查询
-
交叉连接
交叉连接返回的结果是被连接的两个表中的所有数据行的笛卡尔积,也就是返回第一个表中符合查询条件的数据行乘以第二个表中符合查询条件的数据行数。在实际开发中这种业务需求是很少见的,一般不会使用交叉连接。
-
内连接
内连接(inner join)又称简单连接或者自然连接,是一种非常常见的连接查询。内连接使用比较运算符对两个表中的数据进行比较,并列出与连接条件匹配的数据行,组合成新的记录,也就是在内连接查询中,只有满足条件的记录才会出现在查询结果集中。
- 隐式内连接:看不到inner join关键字而是使用where关键字代替。
- 显式内连接:语句明显地调用了inner join关键字。
-
外连接
前面讲解的内连接查询中,返回的结果只包含符合查询条件和连接条件的数据,然而有时还需要包含没有关联的数据即返回的数据不仅要包含符合条件的数据,而且还要包括左表、右表或者两个表中所有的数据,此时就需要使用外连接查询。
-
左连接:返回包含左表中的数据和右表中符合连接条件的记录。
-
右连接:返回包含右表中的数据和左表中符合连接条件的记录。
-
-
Hibernate进行多表查询与SQL其实是相似的,但是HQL会在原来SQL分类的基础上又多出来一些操作。
-
交叉连接
-
内连接
- 显式内连接
- 隐式内连接
- 迫切内连接
-
外连接
- 左外连接
- 迫切外连接
- 右外连接
与传统SQL不同的是,HQL多了迫切内连接、迫切外连接两个类型,使用上的区别就是**迫切内(外)连接就是在内(外)连接的join后添加一个fetch关键字。**fetch关键字的主要区别就在封装数据,因为他们的查询结果集是一样的,生成的底层SQL语句也相同。
- 内(外)连接:发送的就是内(外)连接的语句,封装的时候将属于各自对象的数据封装到各自的对象中,最后得到一个List<Object[]>。
- 迫切内(外)连接:发送的是内(外)连接的语句,需要在编写HQL的时候在join后增加一个fetch关键字,从而将每条数据封装到对象中,最后得到一个List。
但是迫切内连接封装以后会出现重复的数据,者需要我们手动剔除。
Hibernae的抓取策略
在实际开发中很多场景下都需要用到查询操作,但是Hibernate本身的效率不太好,尤其是在获取关联对象的时候,那么我们需要对一些查询语句优化,才能更好地去提升性能。那么该如何优化呢,主要依赖于为我们即将介绍到的抓取策略。
抓取策略是当应用程序需要在关联关系中进行导航的时候,Hibernate如何获取关联对象的策略。Hibernate的抓取策略是Hibernate提升性能的一种手段,但是往往抓取策略需要和延迟加载一起使用。通常延迟加载分为两类:类级别延迟、关联级别延迟。
-
类级别延迟加载
使用load方法检索某个对象的时候,这个类是否采用延迟加载的策略,就是类级别的延迟。类级别的延迟一般在上配置lazy属性,默认是true,所以使用load方法去查询的时候,不会立马发送SQL语句,当真正使用该对象的时候才会发送SQL语句。
-
关联级别延迟加载:
Customer cusomer = session.get(Customer.class,1); Set<LinkMan> linkmans = customer.getLinkMans();
通过客户查询其关联的联系人对象,在查询联系人的时候是否采用延迟加载是关联级别的延迟。关联级别的延迟通常在set和many-to-one上来配置的。
set标签上的lazy通常有三个取值:
- true:默认值,采用延迟加载
- false:不采用延迟加载
- extra:及其懒惰的
many-to-one标签上的lazy通常有三个取值:
- proxy:默认值,是否采用延迟取决于一的一方类上的lazy属性值
- fasle:不采用延迟加载
- on-proxy:不用研究
延迟加载已经介绍过了,现在该介绍抓取策略了。它指的是查询到某个对象的值的时候如何抓取关联对象,这个也可以通过配置完成。在关联对象的标签上配置fetch的值。关联上就分为和上,也有不同值。
set标签上的fetch通常有三个取值:
- select:默认值,发送的是普通的select语句查询
- join:发送一条迫切左外连接取查询
- subselect:发送一条自查询语句查询其关联对象
many-to-one标签上的fetch有两个取值:
- select:默认值,发送的是普通的select语句查询关联对象
- join:发送一条迫切左外连接取查询关联对象
我们可以简单的总结一下fetch和lazy的作用,fetch主要控制抓取关联对象的时候的发送SQL的格式,lazy主要控制其关联对象的时候是否采用延迟加载的。
在抓取策略中有一种叫做批量抓取,就是同时查询多个对象的关联对象的时候,可以采用批量抓取进行优化,但不是特别重要。只需要在配置文件中配置batch-size的值即可,一般来说数据量越大效果越明显。