Hibernate的简介及工作原理

本文介绍了Hibernate数据库持久化框架的基本概念,并深入探讨了其Session的工作原理,包括如何建立对象与数据库之间的映射,以及如何通过HQL进行数据查询。通过对Hibernate的学习,读者将能更好地理解和应用该框架进行数据库操作。

Hibernate

Hibernate简介
Hibernate是一个ORM框架,突出特点就是强大、难学、开发迅速,适合开发中小型的、没有复杂关联关系的、业务
逻辑相对固定的项目。
Hibernate 四个核心部分:持久化操作、关联关系管理、Hibernate查询语言和二级缓存。

Hibernate持久化操作
Hibernate可以把对实体对象的操作转化为对数据库的操作。比如调用session.save(entity)时,Hibernate内部
会自动生成并执行insert语句来把数据保存到数据库。

主配置文件
主配置文件用来指定Hibernate全局参数,以及加载实体类的映射文件,习惯命名为hibernate.cfg.xml
由于不同数据库支持的SQL语句在细节上有差别,Hibernate在生成SQL语句时就要参照具体的数据库细节,这称为
数据库方言。虽然Hibernate能够识别底层所用数据库,但数据库会有多个版本,Hibernate默认选择的方言版本可
能不是我们想要的,所以最好自己指定数据库方言。
至于数据库连接池,Hibernate和Spring整合时会交给Spring管理,就不再单独配置,这样的话使用的是
Hibernate内置的数据库连接池(不可在生产环境中使用)。

映射文件
Hibernate的ORM映射包含很多方面,其中一些需要在映射文件中进行配置,比如:
类——表,表现在类名和表名相对应
字段——列,表现在字段名和列名相对应
对象——行,表现在OID和主键相对应

在Hibernate中用来唯一标志实体对象的字段称为OID,一般使用id字段作为OID,Hibernate使用OID判断两个实体
对象是否对应同一行数据。

映射文件习惯上命名为:类名.hbm.xml,DTD约束文件在org.hibernate包下
<class>用来映射类和表
<id>用来映射OID和主键列
<generator>用来指定主键生成策略
主键生成策略                    适用类型                            说明
identify                       int、long              使用自动递增主键生成主键值,比如MySQL
sequence                       int、long              使用序列生成主键值,比如oracle
native                         int、long              根据数据库自动选择identity或者sequence
uuid                           String                 由Hibernate生成UUID主键值
increment                      int、long              由Hibernate生成递增主键值
assigned                       String、int、long      由开发人员自己生成主键值

<property>用来映射字段和列,至于字段类型和列类型,Hibernate一般都能正确判断,但对于Date类型的字段最
好使用type属性指定想要的列类型,如date、time、datetime。

核心API
Hibernate有两个核心类:SessionFactory和Session。

SessionFactory
SessionFactory用来创建Session,一个SessionFactory对象对应一个数据库,如果项目中需要访问多个数据库,
就需要配置、创建多个SessionFactory。
项目中一般把SessionFactory配置成spring的bean,由spring创建、管理,所以学习阶段创建SessionFactory
的代码无需记忆,直接copy即可。
创建SessionFactory时会加载并解析主配置文件和映射文件,而且这个时候很多常用SQL语句已经生成。

Session
Session表示和数据库的一次会话、一次连接,内部封装了java.sql.Connection。
Session提供了save()get()、update()delete()等方法,以便对实体对象进行持久化操作,session进行持
久化操作需要依赖其内部的持久化上下文,也可以简单的认为session就是持久化上下文。
在项目中,由于事务会交给Spring管理,Spring可以根据配置正确的给持久化操作提供事务支持,所以项目中一般
不需要手动编写事务代码。
说明:为了简化书写,后面的entity都表示实体对象

Session工作原理:
实体对象状态转换图
持久化上下文结构图

实体对象状态 是相对于session来说的,如果session不存在或者已经关闭,实体对象状态也就没有意义。而且实体
对象状态是相对于单个session来说的,一个实体对象在sessionA中的状态和在sessionB中的状态没有任何关系。

瞬时状态 实体对象和当前session没有任何联系(session没有持有实体对象的引用)。
托管状态 一级缓存和档案区中都包含该实体对象的引用,并且档案区中该实体对象的状态标记为MANAGED。
被删除状态 一级缓存和档案区中都包含该实体对象的引用,并且档案区中该实体对象的状态标记为DELETED。

一级缓存 每个session都有自己的一级缓存,存放处于托管状态或者被删除状态的entity的引用,主要是方便持久
化操作时Hibernate内部查找entity。一级缓存的生命周期只和session有关,session创建时创建,session关闭
时关闭,和事务没有关系。一个session在提交上一个事务后可以再开启新事务,这些事务共享同一个一级缓存。

保存
session.save(entity) 执行保存操作,最终就是执行insert语句向表中插入数据
具体执行过程为:
1 立即执行对应的insert语句,并把新生成的主键值查询出来赋值给OID
2 把entity转变为托管状态,也就是把entity放入一级缓存和档案区,并设置状态标记为MANAGED

加载
session的get()执行加载操作
具体执行过程为:
1 先从一级缓存中查找
(如果找到了;2 再检查其状态标记是否是DELETED3 如果是,表示该entity已经被删除,返回null,否则返回
该entity)。
(如果没找到;2 再从数据库中查找(也没找到则返回null); 3 如果找到了,把entity转变为托管状态并返回
该entity)。

修改
修改操作对应着执行update语句根据OID修改表中其他列,但无法修改OID本身,其他字段的值无论是否为null,都
会更新到数据库中。
修改有两种方式:
1 直接修改托管状态下的entity的字段。
Hibernate会在事务提交前检查entity和快照数据是否一致,如果不一致,就会执行update语句。事务提交后,快照
数据更新为entity当前数据。
把update语句推迟到事务提交前执行的好处是可以合并多次操作,以及过滤无效操作,如多次修改同一个字段等。
2 调用update(entity)修改瞬时状态下的实体对象,要求OID不为null。
具体的执行过程为:
1 先把entity转变为托管状态
2 在事务提交前(总会)执行update语句(不会对比快照),事务提交后快照更新为entity当前数据

删除
session.delete(entity)执行删除操作,最终就是执行delete语句根据OID从表中删除数据。
具体执行过程为:
1 (如果entity为瞬时状态,则先转变为托管状态)
2 把状态标记设置为被删除状态
3 事务提交前执行delete语句,事务提交后,一级缓存和档案区中有关数据也会被删除,此时entity转变成瞬时
  状态。
对于被删除状态的entity,逻辑上已经不存在了,之前所做的其他操作就变成了无效操作,之后则不能再执行
update等持久化操作(但普通操作如访问entity的字段和方法是可以的)。

延迟加载
Session的get()第一次执行时会立即从数据库中查询数据,而load()第一次执行时则会返回一个只有OID的代理
对象,查询动作会推迟到第一次调用代理对象的方法时才真正进行,这称为延迟加载(注意:调用代理对象的
hashCode()或eqauls()时并不会触发查询动作)。
当load()查询不到数据时会抛出ObjectNotFoundException异常
当session关闭后再触发查询动作时会抛出LazyInitializationException异常
因为load()牵涉到代理对象、延迟加载等,具体执行细节就相对复杂一些,但前面所讲的一级缓存以及档案区仍然
是有效的,Hibernate会正确处理好各种持久化操作。
延迟加载属于数据库访问优化技术,单看load()好像优化效果并不明显,其实其效果主要体现在关联对象的加载上面

关联关系管理
表之间的关联关系由外键列来维护,主要有一对多(多对一)、多对多,体现在Java对象中就是关联字段,比如
User类中有Set<Question>字段,或者Question类中有User字段。
单看“User类中有Set<Question>字段”并不能确定是一对多还是多对多,还需要参考Question类或者参考对应的表
的外键才能确定。

单向、双向
表之间的关联关系无所谓方向,单向或者双向是相对于程序来说的。
对于Hibernate,如果在映射文件中只配置了User类中的Set<Question>字段,而没有配置Question类中的Use
r字段,这时候程序就只支持通过User对象加载关联的Question对象,而不支持通过Question对象加载关联的
User对象。这时可以说这个关联关系是单向的,而且是单向一对多。

单向一对多
在Hibernate的实体类中一般使用Set字段,而List字段一般用在要求元素具有顺序的场景。
<set>用来配置Set字段
name         指定Set字段名称
table           指定外键列所在的表
inverse  
<key>用来指定外键列的名称
<one-to-many>用来指定此关联关系为一对多
class         指定关联对象的类型,即Set的元素的类型
Java关联对象之间是平等的,并不像表一样有主表、从表之分。
Hibernate可以通过操作外键来正确维护表之间的关联关系。

关联关系操作
Hibernate进行关联关系操作时,有基本效果和级联效果。

基本操作
1 新增关联关系时,如给Set字段添加新元素,对应着给从表相关行的外键列赋值
2 加载entity时,会自动(延迟)加载entity的关联对象
3 删除关联关系时,如从Set字段删除元素,对应着把从表相关行的外键列置为null
4 删除entity时,会把从表中相关行的外键置为null
基本效果是Hibernate通过SQL语句主动完成的,不受数据库自身设定的级联策略限制

级联效果
级联效果由Hibernate提供的级联策略决定,内部也是通过SQL语句主动完成的,也不受数据库自身设定的级联策略
限制。
save-update: save()、update()等操作,对于新增关联关系,如给Set字段添加新元素,新元素可以不手动
save(),会被级联save()deletedelete(entity)时,entity内部的关联对象也会被级联delete()
all: 即 save-update + delete
none: 默认值,没有级联效果
无论指定的级联策略是什么,基本效果都不受影响。
级联删除时,从表中相关行的外键列先被置为null(基本效果),然后这些相关行又被级联删除(级联效果),
可见delete级联策略会对数据库进行额外的删除操作。
由于delete级联策略会级联删除主表或从表中的相关行,使用时需谨慎。比如在delete级联策略下删除班级时,
会把关联的学生也级联删除,又可能把学生关联的其他数据也级联删除...这将导致灾难性的后果,所以delete级联
策略最好只在需要使用并且被级联删除的对象没有其他关联关系时使用。

单项多对一
<many-to-one>用来配置多对一关联关系
name: 指定关联字段的字段名
column: 指定从表(本表)中外键列的名称
cascade: 指定级联策略,级联效果和一对多级联效果一致
class: 指定关联字段类型(Hibernate可以自动识别,一般不需要设置)
Java对象并不像表一样分主表、从表,单向一对多和单向多对一关联操作的基本效果和级联效果是一致的。

双向一对多
双向一对多和双向多对一含义相同,都是单向一对多 + 单向多对一,并且级联效果和单向关联是一样的。
由于是双向关联,访问关联数据就特别方便。
Hibernate可以正确处理要加载的数据已经在一级缓存中的情况,所以不会出现一份数据在一个session中对应多个
实体对象的情况。
由于关联对象的加载默认是延迟的,只在用到的时候才执行select语句,所以不用担心会执行太多无用的select语句
建议使用双向关联

多对多
多对多关联关系一般使用中间表处理,中间表中有两个外键列,分别引用两个主表的主键。同样也建议使用双向关联
以用户、角色举例说明:一个角色可拥有多个用户,一个用户也可拥有多个角色,它们之间就是多对多关联关系。
<set>用来映射Set字段
name: 指定Set字段名称
table: 指定关联关系的外键所在的表(多对多关联时就是中间表)
inverse (默认为false): nverse=true的作用就是当前类放弃维护此关联关系,也就是使用当前类进行关联
操作时,Hibernate不会维护、修改外键列的值。
双向一对多关联不设置此属性,并且Java对象相互引用时,会导致两边都修改外键列的值而造成多执行几条update
语句,但不会导致错误。
双向多对多关联不设置此属性,并且Java对象相互引用时,会导致两边都往中间表中插入相同数据,这会造成错误
综上所述,在双向一对多关联中可以不使用inverse,在双向多对多关联中必须指定其中一方inverse=true,并且
这一方不应该再进行关联操作。
<key>用来指定外键列的名称(中间表中引用本表主键的外键列)
<many-to-many>用来指定此关联关系为多对多
column: 指定外键列的名称(关联对象的表在中间表中的外键列)
class: 指定关联对象的类型,即Set元素的类型
一对多和多对多的级联效果是一样的。
此外Hibernate还支持一对一关联关系,也支持继承映射(实体类有子类),但用的不多。

HQL查询语言
Hibernate Query Language(HQL),是Hibernate提供的和SQL很相似的查询语言,主要区别是SQL查询时使用
表名和列名,而HQL查询时使用类名和字段名。
不同的select子句会返回不同类型的结果。
查询单个列,查询结果为List<Object>
查询多个列,查询结果为List<Object[]>
查询整个实体类或者不写select子句,查询结果为List<User>
这种方式查询出来的实体对象处于托管状态,而且可以访问它们的关联对象(延迟加载)。
结果集中只可能有一行数据的情况
凡是只会查出一行数据的情况,返回结果类型只和hql语句有关。

条件查询
在Hibernate中,查询条件中占位符主要有两种表示方式:普通问号方式和冒号命名方式。在给占位符赋值时,普通
方式使用索引位置赋值,命名方式则使用名称赋值。
当查询条件是动态的,比如用户可能使用姓名或者生日查询,这时HQL查询方式就不方便,可以使用Hibernate提供
的QBC查询方式。
HQL的多表连接查询较为繁琐,其实HQL返回的实体对象可以延迟加载关联对象,实际上已经隐式的实现了连接查询
的效果。

分页查询
由于各个数据库分页SQL差别很大,建议使用Hibernate提供的统一的分页查询方式。

命名查询
以上HQL查询时都是把HQL语句写在代码中,为了方便管理,也可以把这些HQL语句写在配置文件中,这种方式称为
命名查询,这点和Mybatis有点相似。
新版本的HQL加入了对update、delete的支持,以便进行批量修改、删除。

其他查询方式
除了HQL查询方式外,Hibernate还支持原生SQL查询以及QBC查询。
原生SQL查询返回的结果是散列数据。
同样的,原生SQL查询方式也支持命名查询。
如果查询需求特别复杂可以使用原生SQL进行查询,但这种方式不能跨数据库。

QBC查询
QBC(Query By Criteria)是一种全新的、更加面向对象的查询方式,需要掌握较多的API才能灵活使用。
QBCHQL的查询效果基本一致,相对来说HQL更普遍一些。另外QBC非常适合做动态条件查询。
HQLSQLQBC这三种查询方式在使用时可以选择以HQL或者QBC为主,在一些特殊情况下辅助使用其他查询方式。

二级缓存
Hibernate二级缓存是应用程序级别(全局)的缓存,对所有的Session都共享。相较于Mybatis二级缓存
(命名空间级别),Hibernate二级缓存更具实用意义也更加强大高效。
Hibernate二级缓存默认是关闭的,而且Hibernate只提供了二级缓存接口,并没有提供实现,目前市面上的
第三方实现有很多,如ehcache、oscache、Jbosscache等。

使用二级缓存

整合ehcahe
先添加Hibernate整合ehcache的jar包依赖(已添加),然后准备好ehcache的配置文件(不是必须的)
maxElementsInMemory指定内存中最多可缓存多少个对象数据
overflowToDisk指定当内存中缓存的个数超出限制时,是否把对象数据转存到硬盘
<diskStore>指定把对象数据转存到硬盘时存放的目录
eternal指定缓存的数据是否永久有效
timeToIdleSeconds指定缓存数据可空闲的时间(eternal=false时有效)
timeToLiveSeconds指定缓存数据可存活的时间(eternal=false时有效)

开启二级缓存总开关
决定哪个实体类的对象会被缓存
要想把实体对象放入二级缓存,需要开启实体类的对象缓存开关
<cache>的usage属性指定缓存的读取策略
read-only: 只读缓存,从二级缓存获得的实体对象不可被修改
nonstrict-read-write: 非严格读写,从二级缓存获得的实体对象可被修改,但修改后缓存失效
read-write: 支持读写,从二级缓存获得的实体对象可被修改,且修改后缓存仍然有效
transactional: (不做要求)
如果对性能要求不太高,可使用read-write,否则使用nonstrict-read-write,而read-only很少使用。

二级缓存内部结构
二级缓存主要有三个缓存区域(Map):对象缓存区域、集合缓存区域和查询缓存区域

对象缓存区域
对象缓存区域是二级缓存的基本缓存区域,适用于缓存实体对象。
当加载实体对象时,比如get(),Hibernate会先从一级缓存中查找,如果找不到再从二级缓存中查找,如果还找
不到就从数据库中查找。
这个查找顺序和mybatis不同(mybatis先从二级缓存中查找),这是因为缓存结构以及操作方式不同造成的。

对象缓存区域的操作过程为:
以User为例说明
1 当第一次执行get()等操作时,把最终从数据库加载到的User对象先放入一级缓存
2 然后再取出User对象字段的值封装成Object[](散装数据)
3 以User类全类名+OID作为key,以Object[]作为value,一并放入对象缓存区域
4 当另一个session再次执行get()时,先从其一级缓存中查找(这时找不到)
5 然后使用全类名和OID从二级缓存的对象缓存区域中查找并取出Object[]
6 把Object[]重新封装成User对象并返回

集合缓存区域
需要开启集合缓存开关(和对象缓存开关相互独立)
集合缓存区域只针对实体对象的集合字段,而集合字段和一级缓存没有关系,所以讲集合缓存区域时无需关注一级
缓存。

集合缓存区域的操作过程:
以User为例说明
1 当第一次访问user.questions最终从数据库延迟加载到Set<Question>
2 如果Question开启了对象缓存,则先把Set<Question>中每个元素都放入对象缓存区域(没有开启就不放)
3 把Set<Question>的每个元素的OID抽取出来封装成List<OID>
4 以entity+questions字段名作为key,以List<OID>作为value,一并放入集合缓存区域
5 当另一个session再次访问user.questions时,会从集合缓存区域中取出List<OID>
6 遍历List<OID>,使用OID从一级缓存或者二级缓存或者数据库中查找Question对象
7最后把所有的Question对象放入Set<Question>并赋值给user.questions

查询缓存区域
查询缓存的开关是全局的,具体哪条查询语句需要缓存则使用代码控制。
以User为例说明
1 第一次执行某个HQL查询并返回了List<User>
2 如果User类开启了对象缓存,则先把List<User>中每个User放入对象缓存区域(没开启就不放)
3 把List<User>中每个User的OID抽取出来封装成List<OID>
4 以实际执行的SQL+查询参数值作为key,以List<OID>作为value,放入查询缓存区域
5 当再次执行该HQL查询并且查询参数值也相同时,就会从查询缓存中取出List<OID>
6 遍历List<OID>,使用OID从一级缓存或者二级缓存或者数据库中查找User对象
7 最后把所有的User对象放入一个List<User>中并返回

Struts2+Spring+Hibernate整合

struts2+Spring整合需要做的事情:
1 在pom.xml中加入struts2、Spring依赖以及struts-spring整合插件依赖
2 在web.xml配置初始化struts2核心filter和Spring容器的监听器
3 提供beans.xml配置文件,并配置扫描action类所在的包
4 在action类上标注@Controller和@Scope("prototype")
5 在struts.xml中通过常量指定使用Spring创建action对象,并且<action>标签的class属性值为Spring bean
的name。

Spring+Hibernate整合:

1 添加Hibernate等jar包依赖和配置文件
2 让Spring管理SessionFactory、事务等
3 管理session的生命周期
4 封装通用DAO5 封装通用Service类
6 整合测试
在beans.xml中配置SessionFactory、HibernateTemplate、事务管理
HibernateTemplate是Spring为了方便整合Hibernate专门提供的工具类,内部对session持久化操作进行了封装,
所提供的工具方法和session的基本一致,不过对于查询等操作,则利用HibernateCallback回调开发人员自己
编写的查询逻辑。

管理session的生命周期
Hibernate在和Spring整合时,session的生命周期也交给了Spring进行管理,默认和事务范围一致,一般service
层方法结束后session就被关闭。但session关闭后实体对象在controller层或view层(JSP)延迟加载关联对象
时,就会出错。
为了解决这个问题,Spring提供了OpenSessionInViewFilter,它会在拦截到请求时就开启一个和当前线程绑定
的session,并且该session会在生成响应后才关闭,这样就扩大了session的生命周期(但并不会影响事务的范围)
,从而使得延迟加载可以正常使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值