Hibernate基础知识汇总

2015-06-19 Eric Wang 更多博文 » 博客 » GitHub »

Hibernate

原文链接 http://codepub.cn/2015/06/19/Basic-knowledge-summary-of-Hibernate/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


logo

ORM简介

ORM(Object/Relation Mapping),对象关系映射,ORM是一种规范,主要完成面向对象的编程语言到关系数据库的映射。 ORM框架是面向对象程序设计语言与关系数据库发展不同步时的中间解决方案。ORM工具的唯一作用就是:把对持久化对象的保存、删除、修改等操作,转换成对数据库的操作,从此,就可以以面向对象的方式操作持久化对象,而ORM框架则负责转换成对应的SQL操作。

数据源简介

数据源是一种提高数据库连接性能的常规手段,数据源会负责维持一个数据连接池,当程序创建数据源实例时,系统会一次性地创建多个数据库连接,并把这些数据库连接保存在连接池中。当程序需要进行数据库访问时,无需重新获得数据库连接,而是从连接池中取出一个空闲的数据库连接。当程序使用数据库连接访问数据库结束后,无需关闭数据库连接,而是将数据库连接归还给连接池即可。通过此种方式,可以避免频繁地获取数据库连接、关闭数据库连接所导致的性能下降。

Hibernate体系结构

Hibernate的体系架构如下所示

architecture

下面对上图中各对象逐一解释

  • SessionFactory: 这是Hibernate的关键对象,它是单个数据库映射关系经过编译后的内存镜像,也是线程安全的。它是生成Session的工厂,本身需要依赖于ConnectionProvider。该对象可以在进程或集群的级别上,为那些事务之间可以重用的数据提供可选的二级缓存
  • Session: 它是应用程序与持久存储层直接交互的一个单线程对象。它也是Hibernate持久化操作的管家对象,所有的持久化对象必须在Session管理下才可以进行持久化操作。此对象生存期很短。它底层封装了JDBC连接,它也是Transaction的工厂 。Session对象持有必选的一级缓存,在显式执行flush之前,所有持久化操作的数据都在缓存中的Session对象处
  • PO(Persistent Object): 系统创建的POJO实例,一旦与特定的Session关联,并对应数据表的指定记录,该对象就处于持久化状态,这一系列对象都被称为持久化对象。在程序中对持久化对象执行的修改,都将自动被转换为对持久层的修改。持久化对象完全可以是普通的JavaBeans/POJO,唯一的区别是它们正与一个Session关联
  • 瞬态对象和脱管对象: 系统通过new关键字创建的Java实例,没有与Session关联,此时处于瞬态。瞬态实例可能是被应用程序实例化后,尚未进行持久化的对象。如果一个曾经持久化过的实例,如果Session被关闭则转为脱管状态
  • 事务(Transaction): 代表一次原子操作,它具有数据库事务的概念。Hibernate事务是对底层具体的JDBC、JTA以及CORBA事务的抽象。在某些情况下,一个Session之间可能包含多个Transaction对象。虽然事务操作是可选的,但所有持久化操作都应该在事务管理下进行,即使是只读操作
  • 连接提供者(ConnectionProvider): 它是生成JDBC连接的工厂,它通过抽象将应用程序与底层DataSource或DriverManager隔离开。这个对象无需应用程序直接访问,仅在应用程序需要扩展时使用
  • 事务工厂(TransactionFactory): 它是生成Transaction对象实例的工厂。该对象也无需应用程序直接访问。它负责对底层具体的事务实现进行封装,将底层具体的事务抽象成Hibernate事务

深入理解持久化对象

持久化类的要求

  • 提供一个无参数的构造器: 所有的持久化类都应该提供一个无参数的构造器,这个构造器可以不采用public访问控制符。只要提供了无参数的构造器,Hibernate就可以使用Constructor.newInstance()来创建持久化类的实例了。通常构造器的访问控制修饰符至少是包可见的
  • 提供一个标识属性: 标识属性通常映射数据库表的主键字段,对于基本类型,建议使用其对应的包装类型
  • 为持久化类的每个成员变量提供setter和getter方法: Hibernate默认采用属性方式来访问持久化类的成员变量
  • 使用非final的类: 在运行时生成代理是Hibernate的一个重要功能,如果非要使用一个有public final方法的类,则必须通过设置lazy="false"来明确地禁用代理
  • 重写equals()和hashCode()方法: 如果需要把持久化类的实例放入Set中,则应该为持久化类重写equals()和hashCode()方法

持久化类对象的状态

life-cycle

在Hibernate中,PO(Persistent Object)有如下三种状态

  • 瞬态: 如果PO实例从未与Session关联过,该PO实例处于瞬态状态,瞬态对象不会被持久化到数据库中,也不会被赋予持久化标识
  • 持久化: 如果PO实例与Session关联起来,且该实例对应到数据库记录,则该实例处于持久化状态
  • 脱管: 如果PO实例曾经与Session关联过,但因为Session的关闭等原因,PO实例脱离了Session管理,这种状态为脱管状态,脱管对象的引用仍然有效,对象可继续被修改。如果重新让脱管对象与某个Session关联,这个脱管对象会重新转换为持久化状态,而脱管期间的改动不会丢失,也可被写入数据库

更改持久化对象状态的方法

持久化实体

  • save(): 该方法返回持久化对象的标识属性值(即对应记录的主键值),执行save方法时会立即将持久化对象对应的数据插入数据库
  • persist(): 该方法保存持久化对象,没有任何返回值,当该方法在一个事务外部被调用时,并不立即转换成insert语句

加载持久化实体

  • load(): 该方法具有延迟加载功能,其不会立即访问数据库,当试图加载的记录不存在时,load方法可能返回一个未初始化的代理对象
  • get(): 该方法总是立即访问数据库,当试图加载的记录不存在时,get方法将直接返回null

更新持久化实体 程序对持久化实例所做的修改会在Session flush之前被自动保存到数据库,无需程序调用其他方法来将其持久化。也就是说,修改对象最简单的方法就是在Session处于打开状态时load()它,然后直接修改即可。

更新脱管实体 当程序使用update()来保存程序对持久化对象所做的修改时,如果不清楚该对象是否曾经持久化过,那么程序可以选择使用updateOrSave()方法,该方法自动判断该对象是否曾经持久化过,如果曾经持久化过,就执行update()操作;否则执行save()操作。merge()方法不会持久化给定的对象,例如session.merge(object)代码后,object依然不是持久化状态,object依然不会被关联到session上,merge()方法会返回object对象的副本——该副本处于持久化状态。

删除持久化实体 通过Session的delete()方法来删除持久化实例,一旦删除了该持久化实例,则对应的数据记录也将被删除。

Hibernate进阶

Hibernate的关联映射

关联关系大致有两类,一类是单向关系,另一类是双向关系,单向关系可分为

  • 单向1->1
  • 单向1->N
  • 单向N->1
  • 单向N->N

双向关联又可分为

  • 双向1->1
  • 双向1->N
  • 双向N->N

单向N-1关联

单向的N-1关联只需从N的一端可以访问1的一端,程序应该在N的一端的持久化类中增加一个属性,该属性引用1的一端的关联实体,对于N-1关联(不管是单向,还是双向)都需要在N的一端使用@ManyToOne修饰代表关联实体的属性。

单向1-1关联

单向的1-1关联关系,需要在持久化类里增加代表关联实体的成员变量,并为该成员变量增加setter和getter方法。从持久化类的代码上看,单向1-1与单向N-1没有丝毫区别。因为N的一端或者1 的一端都是直接访问关联实体,只需增加代表关联实体的属性即可。对于1-1关联(不管是单向关联,还是双向关联),都需要使用@OneToOne修饰代表关联实体的属性。

单向1-N关联

单向1-N关联的持久化类发生了改变,持久化类里需要使用集合属性。因为1的一端需要访问N的一端,而N的一端将以集合(Set)形式表现。对于单向1-N关联关系,只需要在1的一端增加Set类型的成员变量,该成员变量记录当前实体所有的关联实体。使用@OneToMany注解修饰1的一端对应N的一端的集合。

单向N-N关联

单向的N-N关联和1-N关联的持久化类代码完全相同,控制关系的一端需要增加一个Set类型的属性,被关联的持久化实例以集合形式存在。N-N关联必须使用连接表,N-N关联与有连接表的1-N关联非常相似,因此都需要使用@JoinTable来映射连接表,区别是N-N关联要去掉@JoinTable注解的inverseJoinColumns属性所指定的@JoinColumn中的unique=true

双向1-N关联

对于1-N关联,Hibernate推荐使用双向关联,而且不要让1的一端控制关联关系,而使用N的一端控制关联关系。双向的1-N关联与N-1关联是完全相同的两种情形,两端都需要增加对关联属性的访问,N的一端增加引用到关联实体的属性,1的一端增加集合属性,集合元素为关联实体。 无连接表的双向1-N关联,N的一端需要增加@ManyToOne注解来修饰代表关联实体的属性,而1的一端则需要使用@OneToMany注解来修饰代表关联实体的属性。 对于有连接表的双向1-N关联而言,1的一端无需任何改变;只要在N的一端使用@JoinTable显式指定连接表即可。

双向N-N关联

双向N-N关联需要两端都使用Set集合属性,两端都增加对集合属性的访问。双向N-N关联没有太多选择,只能采用连接表来建立两个实体之间的关联关系。 双向N-N关联需要在两端分别使用@ManyToMany修饰Set集合属性,并在两端都使用@JoinTable显式映射连接表。在两端映射连接表时,两端指定的连接表的表明应该相同,而且两端使用@JoinTable时指定的外键列的列名也是相互对应的。 如果某一段想放弃控制关联关系,则可在这一段的@ManyToMany注解中指定mappedBy属性,这一段就无需、也不能使用@JoinTable映射连接表了。

双向1-1关联

双向1-1关联需要使用@OneToOne注解进行映射,并让两个持久化类都增加引用关联实体的属性,并为该属性提供setter和getter方法。

基于复合主键的关联关系

在实际项目中并不推荐使用复合主键,总是建议采用没有物理意义的逻辑主键。复合主键的做法不仅会增加数据库建模的难度,而且会增加关联关系的维护成本。

持久化的传播性

对于关联实体而言,Hibernate默认不会启用级联操作,当父对象被保存时,它关联的子实体不会被保存;父对象被删除时,它关联的子实体不会被删除。为了启用不同持久化操作的级联关系,Hibernate定义了如下级联风格

  • CascadeType.ALL: 指定Hibernate将所所有的持久化操作都级联到关联实体
  • CascadeType.MERGE: 指定Hibernate将merge操作级联到关联实体
  • CascadeType.PERSIST: 指定Hibernate将persist操作级联到关联实体
  • CascadeType.REFRESH: 指定Hibernate将refresh操作级联到关联实体
  • CascadeType.REMOVE: 指定Hibernate将remove操作级联到关联实体 如果程序希望某个操作能被级联传播到关联实体,则可以在配置@OneToMany@OneToOne@ManyToMany时通过cascade属性来指定。注意级联风格是可组合的。

事务控制

Session与事务

SessionFactory对象的创建代价很高,它是线程安全的对象,被设计成可以被所有线程所共享。通常,SessionFactory会在应用程序启动时创建,一旦创建了SessionFactory就不会轻易关闭,只有当应用退出时才关闭SessionFactory。 Session对象是轻量级的,它也是线程不安全的。对于单个业务进程、单个工作单元而言,Session只被使用一次。创建Session时,并不会立即打开与数据库之间的连接,只有需要进行数据库操作时,Session才会获取JDBC连接。因此,打开和关闭Session,并不会对性能造成很大的影响。

一级、二级缓存

Hibernate包括两个级别的缓存

  • 默认总是启用的Session级别的一级缓存
  • 可选的SessionFactory级别的二级缓存

其中Session级别的一级缓存不需要开发者关心,默认总是有效的,当应用保存持久化实体、修改持久化实体时,Session并不会立即把这种改变flush到数据库,而是缓存在当前Session的一级缓存中,除非程序显式调用Session的flush()方法,或程序关闭Session时才会把这些改变一次性的flush到底层数据库。

SessionFactory级别的二级缓存是全局性的,应用的所有Session都共享这个二级缓存。不过SessionFactory级别的二级缓存默认是关闭的,必须由程序显式开启。一旦在应用程序中开启了二级缓存,当Session需要抓取数据时,Session将会先查找一级缓存,再查找二级缓存,只有当一级缓存和二级缓存中都没有要抓取的数据时,才会去查找底层数据库。

查询缓存

一级、二级缓存都是对整个实体进行缓存,它不会缓存普通属性,如果想对普通属性进行缓存,则可以考虑使用查询缓存。

Notes

需要指出的是,在大部分情况下查询缓存并不能提高应用性能,甚至反而会降低应用性能,因此在实际项目中请慎重使用查询缓存。

对于查询缓存来说,它缓存的key就是查询所用的HQL或SQL语句。需要指出的是,查询缓存不仅要求所使用的HQL或SQL语句相同,甚至要求所传入的参数也相同,Hibernate才能直接从查询缓存中取得数据。

查询缓存默认是关闭的,为了开启需要在hibernate.cfg.xml中配置

<property name="hibernate.cache.use_query_cache">true</property>

除此之外,在程序中还必须调用Query对象的setCacheable(true)才会对查询结果进行缓存。

//设置查询缓存,在第二次查询时不会重新发出SQL语句进行查询
session.createQuery("select name, age from person").setCacheable(true);

事件机制

在Hibernate执行持久化的过程中,应用程序通常都无法参与其中。所有的数据持久化操作,对用户都是透明的,用户无法插入自己的动作。 通过事件框架,Hibernate允许应用程序能响应特定的内部事件,从而允许实现某些通用的功能,或者对Hibernate功能进行扩展。 Hibernate的事件框架由两部分组成

  • 拦截器机制: 对于特定动作拦截,回调应用中的特定动作
  • 事件系统: 重写Hibernate的事件监听器

拦截器

拦截器通过Interceptor接口,可以从Session中回调应用程序的特定方法,这种回调机制可让应用程序在持久化对象被保存、更新、删除或加载之前,检查并修改其属性。 通过Interceptor接口,可以在数据进入数据库之前,对数据进行最后的检查,如果数据不符合要求,则可以修改数据,从而避免非法数据进入数据库。 使用拦截器按如下步骤进行

  • 定义实现Interceptor接口的拦截器类
  • 通过Session启用拦截器,或者通过Configuration启用全局拦截器

事件系统

Hibernate的事件系统完全可以替代拦截器,也可以作为拦截器的补充来使用。 使用事件系统按如下步骤进行

  • 实现自己的事件监听器类
  • 注册自定义事件监听器,代替系统默认的事件监听器

实现用户的自定义监听器有如下三种方法

  1. 实现对应的监听器接口: 实现接口必须实现接口内的所有方法,关键是必须实现Hibernate对应的持久化操作,即数据库访问,这以为这程序员完全取代了Hibernate的底层操作
  2. 继承事件适配器: 可以有选择则行地实现需要关注的方法,但依然试图取代Hibernate完成数据库的访问
  3. 继承系统默认的事件监听器: 扩展特定方法

通常推荐采用第三种方法实现自己的事件监听器。Hibernate默认的事件监听器都被声明成non-final,以便用户继承他们。

参考文献 [1] 轻量级JavaEE企业应用实战-Struts2+Spring4+Hibernate整合开发