Hibernate 开发指南
Original Author: 夏昕<xiaxin@gmail.com>
本文是由笔者2003 年底一个咨询项目中,为客户做的持久层设计培训
文案整理而来。
其中的内容涉及Hibernate 的使用,以及一部分笔者实际咨询项目中的
经验积累,另一方面,大部分是笔者在Hibernate 的官方论坛中与众多
技术专家交流所得。
既来于斯,则归于斯。希望能聊有所用。
本文并非试图替代Hibernate Reference,相对而言,Hibernate Reference
的编写目的是为开发者提供更简便的条目索引,而本文目标则在于为开
发人员提供一个入门和掌握Hibernate的途径。
本文需结合Hibernate Reference使用。
笔者好友曹晓钢义务组织了Hibernate文档的汉化工作,在此对其辛勤劳作致敬。
中文版Hibernate Reference将被包含在Hibernate下个官方Release中,目前可
通过http://www.redsaga.com获取中文版Hibernate Reference的最新版本。
本文中如果发现问题和错误,请随时联系笔者,以免误导他人。
本文转载不限,不过请保持本文完整。万分感谢!
Hibernate 开发指南.......................................................................................................1
准备工作..........................................................................................................3
构建Hibernate基础代码...............................................................................3
由数据库产生基础代码...........................................................................4
Hibernate配置..............................................................................................15
第一段代码....................................................................................................17
Hibernate基础语义......................................................................................19
Configuration ........................................................................................19
SessionFactory.......................................................................................20
Session....................................................................................................20
Hibernate高级特性......................................................................................................22
XDoclet与Hibernate映射...........................................................................22
数据检索........................................................................................................31
Criteria Query...............................................................................31
Criteria查询表达式................................................................31
Criteria高级特性....................................................................33
限定返回的记录范围.............................................................33
对查询结果进行排序.............................................................33
Hibernate Query Language (HQL).........................................34
数据关联........................................................................................................35
一对一关联.............................................................................35
一对多关联.............................................................................37
Ø 单向一对多关系......................................................37
Ø 双向一对多关系......................................................42
多对多关联.............................................................................47
数据访问........................................................................................................54
PO和VO...............................................................................................54
关于unsaved-value ...............................................................................57
Inverse和Cascade.........................................................................59
延迟加载(Lazy Loading)............................................................59
事务管理........................................................................................................63
基于JDBC的事务管理:.....................................................................64
基于JTA的事务管理:.......................................................................65
锁(locking).........................................................................................68
悲观锁(Pessimistic Locking).......................................68
乐观锁(Optimistic Locking)..........................................69
Hibernate分页..........................................................................................73
Cache管理....................................................................................................75
Session管理...............................................................................................79
编后赘言................................................................................................................84
Hibernate Quick Start
准备工作
1. 下载Ant软件包,解压缩(如C:\ant\)。并将其bin目录(如c:\ant\bin)添加到系统
PATH 中。
2. 下载Hibernate、Hibernate-Extension和Middlegen-Hibernate软件包的最新版本。
http://prdownloads.sourceforge.net/hibernate/
构建Hibernate 基础代码
Hibernate基础代码包括:
1. POJO
POJO 在Hibernate 语义中理解为数据库表所对应的Domain Object。这里的POJO
就是所谓的“Plain Ordinary Java Object”,字面上来讲就是无格式普通Java 对象,简
单的可以理解为一个不包含逻辑代码的值对象(Value Object 简称VO)。
一个典型的POJO:
public class TUser implements Serializable {
private String name;
public User(String name) {
this.name = name;
}
/** default constructor */
public User() {
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
2. Hibernate 映射文件
Hibernate 从本质上来讲是一种“对象-关系型数据映射”(Object Relational
Mapping 简称ORM)。前面的POJO在这里体现的就是ORM中Object层的语义,
而映射(Mapping)文件则是将对象(Object)与关系型数据(Relational)相关联
的纽带,在Hibernate中,映射文件通常以“.hbm.xml”作为后缀。
构建Hibernate基础代码通常有以下途径:
1. 手工编写
2. 直接从数据库中导出表结构,并生成对应的ORM文件和Java 代码。
这是实际开发中最常用的方式,也是这里所推荐的方式。
通过直接从目标数据库中导出数据结构,最小化了手工编码和调整的可能性,从而
最大程度上保证了ORM文件和Java 代码与实际数据库结构相一致。
3. 根据现有的Java 代码生成对应的映射文件,将Java 代码与数据库表相绑定。
通过预先编写好的POJO 生成映射文件,这种方式在实际开发中也经常使用,特别
是结合了xdoclet 之后显得尤为灵活,其潜在问题就是与实际数据库结构之间可能
出现的同步上的障碍,由于需要手工调整代码,往往调整的过程中由于手工操作的
疏漏,导致最后生成的配置文件错误,这点需要在开发中特别注意。
结合xdoclet,由POJO 生成映射文件的技术我们将在“高级特性”章节中进行探讨。
由数据库产生基础代码
通过Hibernate官方提供的MiddleGen for Hibernate 和Hibernate_Extension工具包,我
们可以很方便的根据现有数据库,导出数据库表结构,生成ORM和POJO。
1) 首先,将Middlegen-Hibernate软件包解压缩( 如解压缩到C:\Middlegen\ )。
2) 配置目标数据库参数
进入MiddleGen 目录下的\config\database 子目录,根据我们实际采用的数据库打开
对应的配置文件。如这里我们用的是mysql数据库,对应的就是mysql.xml 文件。
<property name="database.script.file"
value="${src.dir}/sql/${name}-mysql.sql"/>
<property name="database.driver.file"
value="${lib.dir}/mysql.jar"/>
<property name="database.driver.classpath"
value="${database.driver.file}"/>
<property name="database.driver"
value="org.gjt.mm.mysql.Driver"/>
<property name="database.url"
value="jdbc:mysql://localhost/sample"/>
<property name="database.userid"
value="user"/>
<property name="database.password"
value="mypass"/>
<property name="database.schema"
value=""/>
<property name="database.catalog"
value=""/>
<property name="jboss.datasource.mapping"
value="mySQL"/>
其中下划线标准的部分是我们进行配置的内容,分别是数据url以及数据库用
户名和密码。
3) 修改Build.xml
修改MiddleGen 根目录下的build.xml 文件,此文件是Middlegen-Hibernate 的Ant
构建配置。Middlegen-Hibernate将根据build.xml 文件中的具体参数生成数据库表映射
文件。可配置的项目包括:
a) 目标数据库配置文件地址
查找关键字 ”!ENTITY”,得到:
<!DOCTYPE project [
<!ENTITY database SYSTEM
"file:./config/database/hsqldb.xml">
]>
默认情况下,MiddleGen 采用的是hsqldb.xml,将其修改为我们所用的数据
库配置文件(mysql.xml):
<!DOCTYPE project [
<!ENTITY database SYSTEM
"file:./config/database/mysql.xml">
]>
b) Application name
查找:
<property name="name" value="airline"/>
“aireline”是MiddleGen原始配置中默认的 Application Name,将其修改为我们
所希望的名称,如“HibernateSample”:
<property name="name" value="HibernateSample"/>
c) 输出目录
查找关键字“name="build.gen-src.dir"”,得到:
<property name="build.gen-src.dir"
value="${build.dir}/gen-src"/>
修改value="${build.dir}/gen-src"使其指向我们所期望的输出目录,
这里我们修改为:
<property name="build.gen-src.dir"
value="C:\sample"/>
d) 对应代码的Package name
查找关键字“destination”,得到:
<hibernate
destination="${build.gen-src.dir}"
package="${name}.hibernate"
genXDocletTags="false"
genIntergratedCompositeKeys="false"
javaTypeMapper=
"middlegen.plugins.hibernate.HibernateJavaTypeMapper"
/>
可以看到,hibernate 节点package 属性的默认设置实际上是由前面的
Application Name (${name})和“.hibernate”组合而成,根据我们的需要,
将其改为:
<hibernate
destination="${build.gen-src.dir}"
package="org.hibernate.sample"
genXDocletTags="true"
genIntergratedCompositeKeys="false"
javaTypeMapper=
"middlegen.plugins.hibernate.HibernateJavaTypeMapper"
/>
这里还有一个属性genXDocletTags,如果设置为true,则生成的代码将包含
xdoclet tag,这为以后在开发过程中借助xdoclet进行映射调整提供了帮助。关
于Hibernate的xdoclet使用,请参见“高级特性”中的相关内容。
注意,如果使用的数据库为SQLServer,需要将build.xml 中如下部分(下划
线部分)删除,否则Middlegen会报出找不到表的错误。
<middlegen
appname="${name}"
prefsdir="${src.dir}"
gui="${gui}"
databaseurl="${database.url}"
initialContextFactory="${java.naming.factory.initial}"
providerURL="${java.naming.provider.url}"
datasourceJNDIName="${datasource.jndi.name}"
driver="${database.driver}"
username="${database.userid}"
password="${database.password}"
schema="${database.schema}"
catalog="${database.catalog}"
>
至此为止,MiddleGen 已经配置完毕,在MiddleGen 根目录下运行ant,就将出现
MiddleGen的界面:
可以看到,数据库中的表结构已经导入到MiddleGen 的操作界面中,选定数据库
表视图中的表元素,我们即可调整各个数据库表的属性。
1 Domain Class Name
对应POJO 的类名
2 Key Generator
主键产生器
可选项说明:
1) Assigned
①
② ③
④
⑤
⑥ ⑦
⑨
⑧
⑩
主键由外部程序负责生成,无需Hibernate参与。
2) hilo
通过hi/lo 算法实现的主键生成机制,需要额外的数据库表保存主
键生成历史状态。
3) seqhilo
与hilo 类似,通过hi/lo 算法实现的主键生成机制,只是主键历史
状态保存在Sequence中,适用于支持Sequence的数据库,如Oracle。
4) increment
主键按数值顺序递增。此方式的实现机制为在当前应用实例中维持
一个变量,以保存着当前的最大值,之后每次需要生成主键的时候
将此值加1作为主键。
这种方式可能产生的问题是:如果当前有多个实例访问同一个数据
库,那么由于各个实例各自维护主键状态,不同实例可能生成同样
的主键,从而造成主键重复异常。因此,如果同一数据库有多个实
例访问,此方式必须避免使用。
5) identity
采用数据库提供的主键生成机制。如DB2、SQL Server、MySQL
中的主键生成机制。
6) sequence
采用数据库提供的sequence 机制生成主键。如Oralce 中的
Sequence。
7) native
由Hibernate根据底层数据库自行判断采用identity、hilo、sequence
其中一种作为主键生成方式。
8) uuid.hex
由Hibernate基于128 位唯一值产生算法生成16 进制数值(编码后
以长度32 的字符串表示)作为主键。
9) uuid.string
与uuid.hex类似,只是生成的主键未进行编码(长度16)。在某些
数据库中可能出现问题(如PostgreSQL)。
10) foreign
使用外部表的字段作为主键。
一般而言,利用uuid.hex 方式生成主键将提供最好的性能和数据库平台适
应性。
另外由于常用的数据库,如Oracle、DB2、SQLServer、MySql 等,都提
供了易用的主键生成机制(Auto-Increase 字段或者Sequence)。我们可以在数
据库提供的主键生成机制上,采用generator-class=native的主键生成方式。
不过值得注意的是,一些数据库提供的主键生成机制在效率上未必最佳,
大量并发insert数据时可能会引起表之间的互锁。
数据库提供的主键生成机制,往往是通过在一个内部表中保存当前主键状
态(如对于自增型主键而言,此内部表中就维护着当前的最大值和递增量),
之后每次插入数据会读取这个最大值,然后加上递增量作为新记录的主键,之
后再把这个新的最大值更新回内部表中,这样,一次Insert操作可能导致数据
库内部多次表读写操作,同时伴随的还有数据的加锁解锁操作,这对性能产生
了较大影响。
因此,对于并发Insert要求较高的系统,推荐采用uuid.hex 作为主键生成
机制。
3 如果需要采用定制的主键产生算法,则在此处配置主键生成器,主键生成器必
须实现net.sf.hibernate.id.IdentifierGenerator 接口。
4 Schema Name
数据库Schema Name。
5 Persister
自定义持久类实现类类名。如果系统中还需要Hibernate 之外的持久层实
现机制,如通过存储过程得到目标数据集,甚至从LDAP中获取数据来填
充我们的POJO。
6 Enable proxies
是否使用代理(用于延迟加载[Lazy Loading])。
7 Dynamic Update
如果选定,则生成Update SQL 时不包含未发生变动的字段属性,这样可
以在一定程度上提升SQL执行效能。
8 Mutable
类是否可变,默认为选定状态(可变)。如果不希望应用程序对此类对应
的数据记录进行修改(如对于数据库视图),则可将取消其选定状态,之
后对此类的Delete和Update操作都将失效。
9 Implement the Lifecyle interface
是否实现Lifecyle接口。Lifecyle接口提供了数据固化过程中的控制机制,
通过实现Lifecyle接口,我们可以在数据库操作中加入回调(Call Back)
机制,如在数据库操作之前,之后触发指定操作。
10 Implement the Validatable interface
是否实现Validatable接口。通过实现Validatable接口,我们可以在数据被
固化到数据库表之前对其合法性进行验证。
值得注意的是,通过实现Lifecyle接口,我们同样可以在数据操作之前验
证数据合法性,不同的是,Validatable 接口中定义的validate 方法可能会
被调用多次,因此设计中应避免在Validatable 接口的validate 方法实现中
加入业务逻辑的验证。
以上是针对Class的设置,同样,在MiddleGen中,我们也可以设定字段属性。在
MiddleGen中选定某个字段,界面下方即出现字段设置栏:
在这里我们可以设置字段的属性,其中:
1 Hibernate mapping specialty
映射类型:
Key :主键
Property :属性
Version :用于实现optimistic locking,参见“高级特性”章节中关
于optimistic locking的描述
2 Java property name
①
②
③
④ ⑤
字段对应的Java 属性名
3 Java Type
字段对应的Java 数据类型
4 Column updateable
生成Update SQL时是否包含本字段。
5 Column insertable
生成Insert SQL时是否包含本字段。
单击窗口顶部的Generate 按钮,MiddleGen 即为我们生成这些数据库表所对应的
Hibernate映射文件。在MiddleGen根目录下的\build\gen-src\net\hibernate\sample目录中,
我们可以看到对应的以.hbm.xml 作为后缀的多个映射文件,每个映射文件都对应了数
据库的一个表。
仅有映射文件还不够,我们还需要根据这些文件生成对应的POJO。
POJO 的生成工作可以通过Hibernate Extension 来完成,Hibernate Extension 的
tools\bin目录下包含三个工具:
1. hbm2java.bat
根据映射文件生成对应的POJO。通过MiddleGen 我们已经得到了映射文件,
下一步就是通过hbm2java.bat工具生成对应的POJO。
2. class2hbm.bat
根据POJO class 生成映射文件,这个工具很少用到,这里也就不再详细介绍。
3. ddl2hbm.bat
由数据库导出库表结构,并生成映射文件以及POJO。这个功能与MiddleGen
的功能重叠,但由于目前还不够成熟(实际上已经被废弃,不再维护),提供
的功能也有限,所以我们还是采用MiddleGen生成映射文件,之后由hbm2java
根据映射文件生成POJO 的方式。
为了使用以上工具,首先我们需要配置一些参数,打开tools\bin\setenv.bat 文件,修改
其中的JDBC_DRIVER和HIBERNATE_HOME环境变量,使其指向我们的实际JDBC Driver
文件和Hibernate所在目录,如
set JDBC_DRIVER=c:\mysql\mysql.jar
set HIBERNATE_HOME=c:\hibernate
同时检查一下环境变量CP中的各个项目中是否实际存在,特别是%CORELIB%下的jar
文件,某些版本的发行包中,默认配置中的文件名与实际的文件名有所出入(如
%CORELIB%\commons-logging.jar, 在Hibernate 发行包中,可能实际的文件名是
commons-logging-1.0.3.jar,诸如此类)。
使用hbm2java,根据MiddleGen生成的映射文件生成Java 代码:
打开Command Window,在tools\bin目录下执行:
hbm2java c:\sample\org\hibernate\sample\*.xml --output=c:\sample\
即可生成对应的POJO。生成的POJO 保存在我们指定的输出目录下(c:\sample)。
目前为止,我们已经完成了通过MiddleGen 产生Hibernate 基础代码的工作。配置
MiddleGen 也许并不是一件轻松的事情,对于Eclipse 的用户而言,目前已经出现了好几个
Hibernate 的Plugin,通过这些Plugin 我们可以更加轻松的完成上述工作,具体的使用方式
请参见附录。
Hibernate 配置
前面已经得到了映射文件和POJO,为了使Hibernate 能真正运作起来,我们还需要一
个配置文件。
Hibernate同时支持xml格式的配置文件,以及传统的properties 文件配置方式,不过这
里建议采用xml 型配置文件。xml配置文件提供了更易读的结构和更强的配置能力,可以直
接对映射文件加以配置,而在properties 文件中则无法配置,必须通过代码中的Hard Coding
加载相应的映射文件。下面如果不作特别说明,都指的是基于xml格式文件的配置方式。
配置文件名默认为“hibernate.cfg.xml”(或者hibernate.properties),Hibernate 初始化期
间会自动在CLASSPATH 中寻找这个文件,并读取其中的配置信息,为后期数据库操作做好
准备。
配置文件应部署在CLASSPATH 中,对于Web 应用而言,配置文件应放置在在
\WEB-INF\classes 目录下。
一个典型的hibernate.cfg.xml配置文件如下:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-2.0.
dtd">
<hibernate-configuration>
<!—- SessionFactory 配置 -->
<session-factory>
<!—- 数据库URL -->
<property name="hibernate.connection.url">
jdbc:mysql://localhost/sample
</property>
<!—- 数据库JDBC驱动 -->
<property name="hibernate.connection.driver_class">
org.gjt.mm.mysql.Driver
</property>
<!—- 数据库用户名 -->
<property name="hibernate.connection.username">
User
</property>
<!—- 数据库用户密码 -->
<property name="hibernate.connection.password">
Mypass
</property>
<!--dialect ,每个数据库都有其对应的Dialet以匹配其平台特性 -->
<property name="dialect">
net.sf.hibernate.dialect.MySQLDialect
</property>
<!—- 是否将运行期生成的SQL输出到日志以供调试 -->
<property name="hibernate.show_sql">
True
</property>
<!—- 是否使用数据库外连接 -->
<property name="hibernate.use_outer_join">
True
</property>
<!—- 事务管理类型,这里我们使用JDBC Transaction -->
<property name="hibernate.transaction.factory_class">
net.sf.hibernate.transaction.JDBCTransactionFactory
</property>
<!—映射文件配置,注意配置文件名必须包含其相对于根的全路径 -->
<mapping resource="net/xiaxin/xdoclet/TUser.hbm.xml"/>
<mapping resource="net/xiaxin/xdoclet/TGroup.hbm.xml"/>
</session-factory>
</hibernate-configuration>
一个典型的hibernate.properties配置文件如下:
hibernate.dialect net.sf.hibernate.dialect.MySQLDialect
hibernate.connection.driver_class org.gjt.mm.mysql.Driver
hibernate.connection.driver_class com.mysql.jdbc.Driver
hibernate.connection.url jdbc:mysql:///sample
hibernate.connection.username user
hibernate.connection.password mypass
第一段代码
上面我们已经完成了Hiberante 的基础代码,现在先从一段最简单的代码入手,感受一
下Hibernate所提供的强大功能。
下面这段代码是一个JUnit TestCase,演示了TUser 对象的保存和读取。考虑到读者可
能没有JUnit的使用经验,代码中加入了一些JUnit相关注释。
public class HibernateTest extends TestCase {
Session session = null;
/**
* JUnit中setUp方法在TestCase初始化的时候会自动调用
* 一般用于初始化公用资源
* 此例中,用于初始化Hibernate Session
*/
protected void setUp(){
try {
/**
* 采用hibernate.properties配置文件的初始化代码:
* Configuration config = new Configuration();
* config.addClass(TUser.class);
*/
//采用hibernate.cfg.xml配置文件
//请注意初始化Configuration时的差异:
// 1.Configuration的初始化方式
// 2.xml文件中已经定义了Mapping文件,因此无需再Hard Coding导入
// POJO文件的定义
Configuration config = new Configuration().configure();
SessionFactory sessionFactory =
config.buildSessionFactory();
session = sessionFactory.openSession();
} catch (HibernateException e) {
e.printStackTrace();
}
}
/**
* 与setUp方法相对应,JUnit TestCase执行完毕时,会自动调用tearDown方法
* 一般用于资源释放
* 此例中,用于关闭在setUp方法中打开的Hibernate Session
*/
protected void tearDown(){
try {
session.close();
} catch (HibernateException e) {
e.printStackTrace();
}
}
/**
* 对象持久化(Insert)测试方法
*
* JUnit中,以”test”作为前缀的方法为测试方法,将被JUnit自动添加
* 到测试计划中运行
*/
public void testInsert(){
try {
TUser user = new TUser();
user.setName("Emma");
session.save(user);
session.flush();
} catch (HibernateException e) {
e.printStackTrace();
Assert.fail(e.getMessage());
}
}
/**
* 对象读取(Select)测试
* 请保证运行之前数据库中已经存在name=’Erica’的记录
*/
public void testSelect(){
String hql=
" from TUser where name='Erica'";
try {
List userList = session.find(hql);
TUser user =(TUser)userList.get(0);
Assert.assertEquals(user.getName(),"Erica");
} catch (HibernateException e) {
e.printStackTrace();
Assert.fail(e.getMessage());
}
}
}
主流IDE,如Eclipse、Intellij IDEA 和JBuilder 中都内置了JUnit支持。下面是Eclipse
中运行该代码的结果(在Run菜单中选择Run as -> JUnit Test即可):
现在我们已经成功实现了一个简单的TUser 实例的保存和读取。可以看到,程序中通过
少量代码实现了Java 对象和数据库数据的同步,同时借助Hibernate的有力支持,轻松实现
了对象到关系型数据库的映射。
相对传统的JDBC数据访问模式,这样的实现无疑更符合面向对象的思想,同时也大大
提高了开发效率。
上面的代码中引入了几个Hibernate基础语义:
1. Configuration
2. SessionFactory
3. Session
下面我们就这几个关键概念进行探讨。
Hibernate基础语义
Configuration
正如其名,Configuration 类负责管理Hibernate 的配置信息。Hibernate 运行时需要
获取一些底层实现的基本信息,其中几个关键属性包括:
1. 数据库URL
2. 数据库用户
3. 数据库用户密码
4. 数据库JDBC驱动类
5. 数据库dialect,用于对特定数据库提供支持,其中包含了针对特定数据库特性
的实现,如Hibernate数据类型到特定数据库数据类型的映射等。
使用Hibernate 必须首先提供这些基础信息以完成初始化工作,为后继操作做好准
备。这些属性在hibernate配置文件(hibernate.cfg.xml 或hibernate.properties)中加以设
定(参见前面“Hibernate配置”中的示例配置文件内容)。
当我们调用:
Configuration config = new Configuration().configure();
时,Hibernate会自动在当前的CLASSPATH 中搜寻hibernate.cfg.xml 文件并将其读
取到内存中作为后继操作的基础配置。Configuration 类一般只有在获取SessionFactory
时需要涉及,当获取SessionFactory 之后,由于配置信息已经由Hibernate 维护并绑定
在返回的SessionFactory之上,因此一般情况下无需再对其进行操作。
我们也可以指定配置文件名,如果不希望使用默认的hibernate.cfg.xml 文件作为配
置文件的话:
File file = new File("c:\\sample\\myhibernate.xml");
Configuration config = new Configuration().configure(file);
SessionFactory
SessionFactory 负责创建Session 实例。我们可以通过Configuation 实例构建
SessionFactory:
Configuration config = new Configuration().configure();
SessionFactory sessionFactory = config.buildSessionFactory();
Configuration实例config会根据当前的配置信息,构造SessionFactory实例并返回。
SessionFactory 一旦构造完毕,即被赋予特定的配置信息。也就是说,之后config 的任
何变更将不会影响到已经创建的SessionFactory 实例(sessionFactory)。如果需要
使用基于改动后的config 实例的SessionFactory,需要从config 重新构建一个
SessionFactory实例。
Session
Session是持久层操作的基础,相当于JDBC中的Connection。
Session实例通过SessionFactory实例构建:
Configuration config = new Configuration().configure();
SessionFactory sessionFactory = config.buildSessionFactory();
Session session = sessionFactory.openSession();
之后我们就可以调用Session所提供的save、find、flush等方法完成持久层操作:
Find:
String hql= " from TUser where name='Erica'";
List userList = session.find(hql);
Save:
TUser user = new TUser();
user.setName("Emma");
session.save(user);
session.flush();
最后调用Session.flush方法强制数据库同步,这里即强制Hibernate将user实
例立即同步到数据库中。如果在事务中则不需要flush方法,在事务提交的时候,
hibernate自动会执行flush方法,另外当Session关闭时,也会自动执行flush方法。
Hibernate高级特性
XDoclet 与Hibernate 映射
在POJO 中融合XDoclet 的映射文件自动生成机制,提供了除手动编码和由数据库导出
基础代码的第三种选择。
本章将结合XDoclet对Hibernate中的数据映射进行介绍。
实际开发中,往往首先使用MiddleGen 和hbm2java 工具生成带有XDoclet tag的POJO
(MiddleGen build.xml中的genXDocletTags选项决定了是否在映射文件中生成XDoclet Tag,
详见Hibernate Quick Start章节中关于MiddleGen的说明)。之后通过修改POJO中的XDoclet
tag进行映射关系调整。
XDoclet已经广泛运用在EJB开发中,在其最新版本里,包含了一个为Hibernate提供支
持的子类库Hibernate Doclet,其中包含了生成Hibernate映射文件所需的ant构建支持以及
java doc tag支持。
XDoclet实现基本原理是,通过在Java代码加入特定的JavaDoc tag,从而为其添加特定
的附加语义,之后通过XDoclet工具对代码中JavaDoc Tag进行分析,自动生成与代码对应
的配置文件,XDoclet。
在Hibernate-Doclet中,通过引入Hibernate相关的JavaDoc tag,我们就可以由代码生成
对应的Hibernate映射文件。
下面是一个代码片断,演示了Hibernate-Doclet的使用方式:
/**
* @hibernate.class
* table="TUser"
*/
public class TUser implements Serializable {
……
/**
* @hibernate.property
* column="name"
* length="50"
* not-null="true"
*
* @return String
*/
public String getName() {
return this.name;
}
……
}
以上是使用Hibernate-Doclet 描述POJO(TUser)及其对应表(TUser)之间映射关系
的一个例子。
其中用到了两个hibernate doclet tag,@hibernate.class和@hibernate.property。
这两个tag分别描述了POJO所对应的数据库表信息,以及其字段对应的库表字段信息。
之后Hibernate Doclet就会根据这些信息生成映射文件:
<hibernate-mapping>
<class
name="net.xiaxin.xdoclet.TUser"
table="TUser"
>
<property
name="name"
type="java.lang.String"
column="name"
not-null="true"
length="50"
>
</class>
</hibernate-mapping>
这样我们只需要维护Java 代码,而无需再手动编写具体的映射文件即可完成Hibernate
基础代码。
熟记Hibernate-Doclet 众多的Tag,显然不是件轻松的事情,好在目前的主流IDE 都提
供了Live Template支持。我们只需进行一些配置工作,就可以实现Hibernate-Doclet Tag
的自动补全功能,从而避免了手工编写过程中可能出现的问题。
附录中提供了主流IDE,包括JBuilder,Intellij IDEA,Eclipse的Hibernate-Doclet集成
指南。
下面我们就Hibernate Doclet 中常用的Tag 进行探讨,关于Tag 的详细参考,请参见
XDoclet 的官方指南(http://xdoclet.sourceforge.net/xdoclet/tags/hibernate-tags.html)以及
Hibernate Reference(http://www.hibernate.org)。
常用Hibernate-Doclet Tag介绍:
1. Class 层面:
1) @hibernate.class
描述POJO 与数据库表之间的映射关系,并指定相关的运行参数。
参数 描述 类型 必须
table 类对应的表名
默认值:当前类名
Text N
dynamic-update 生成Update SQL时,仅包含发生变动
的字段
默认值: false
Bool N
dynamic-insert 生成Insert SQL时,仅包含非空(null)
字段
默认值:false
Bool N
Proxy 代理类
默认值:空
Text N
discriminator-value 子类辨别标识,用于多态支持。 Text N
where 数据甄选条件,如果只需要处理库表中某
些特定数据的时候,可通过此选项设定结
果集限定条件。
如用户表中保存了全国所有用户的数据,
而我们的系统只是面向上海用户,则可指
定where=”location=’Shanghai’"
Text N
典型场景:
/**
* @hibernate.class
* table="TUser" (1)
* dynamic-update="true" (2)
* dynamic-insert="true" (3)
* proxy=”” (4)
* discriminator-value=”1” (5)
*/
public class TUser implements Serializable {
……
}
本例中:
1 table参数指定了当前类(TUser)对应数据库表“TUser”。
2 dynamic-update 参数设定为生成Update SQL 时候,只包括当前发生变化的
字段(提高DB Update性能)。
3 Dynamic-insert 参数设定为生成Insert SQL 时候,只包括当前非空字段。
(提高DB Insert性能)
4 Proxy 参数为空,表明当前类不使用代理(Proxy)。代理类的作用是为Lazy
Loading提供支持,请参见下面关于Lazy Loading的有关内容。
5 discriminator-value参数设为”1”。
discriminator-value 参数的目的是对多态提供支持。请参见下面关于
@hibernate.discriminator的说明。
2) @hibernate.discriminator
@hibernate.discriminator(识别器) 用于提供多态支持。
参数 描述 类型 必须
column 用于区分各子类的字段名称。
默认值:当前类名
text Y
type 对应的Hibernate类型 Bool N
length 字段长度 Bool N
如:
TUser类对应数据库表TUser,并且User类有两个派生类SysAdmin、
SysOperator。
在TUser表中, 根据user_type字段区分用户类型。
为了让Hibernate根据user_type能自动识别对应的Class类型(如 user_type==1
则自动映射到SysAdmin类,user_type==2 则自动映射到SysOperator类),我们需要
在映射文件中进行配置,而在Hibernate-Doclet中,对应的就是
@hibernate.discriminator 标识和 @hibernate.class 以及 @hibernate.subclass 的
discriminator-value属性。
典型场景:
/**
*
* @hibernate.class
* table="TUser"
* dynamic-update="true"
* dynamic-insert="true"
*
* @hibernate.discriminator column="user_type" type="integer"
*/
public class TUser implements Serializable {
……
}
根类TUser 中,通过@hibernate.discriminator 指定了以"user_type"字段
作为识别字段。
/**
* @hibernate.subclass
* discriminator-value="1"
*/
public class SysAdmin extends TUser {
……
}
/**
* @hibernate.subclass
* discriminator-value="2"
*/
public class SysOperator extends TUser {
……
}
SysAdmin 和SysOperator 均继承自TUser,其discriminator-value 分别设置
为"1"和"2",运行期Hibernate 在读取t_user 表数据时,会根据其user_type 字段进行
判断,如果是1 的话则映射到SysAdmin类,如果是2 映射到SysOperator 类。
上例中,描述SysAdmin 和SysOperator 时,我们引入了一个Tag:
@hibernate.subclass,顾名思义,@hibernate.subclass与@hibernate.class
不同之处就在于,@hibernate.subclass 描述的是一个子类,实际上,这两个Tag
除去名称不同外,并没有什么区别。
2. Method层面:
1) @hibernate.id
描述POJO 中关键字段与数据库表主键之间的映射关系。
参数 描述 类型 必须
column 主键字段名
默认值:当前类名
Text N
type 字段类型。
Hibernate总是使用对象型数据类型作
为字段类型,如int对应Integer,因此
这里将id设为基本类型[如int]以避免对
象创建的开销的思路是没有实际意义的,
即使这里设置为基本类型,Hibernate内
部还是会使用对象型数据对其进行处理,
只是返回数据的时候再转换为基本类型
而已。
Text N
length 字段长度 Text N
unsaved-value 用于对象是否已经保存的判定值。
详见“数据访问”章节的相关讨论。
Text N
generator-class 主键产生方式(详见Hibernate Quick
Start中关于MiddleGen的相关说明)
取值可为下列值中的任意一个:
assigned
hilo
seqhilo
increment
identity
sequence
native
uuid.hex
uuid.string
foreign
Text Y
2) @hibernate.property
描述POJO 中属性与数据库表字段之间的映射关系。
参数 描述 类型 必须
column 数据库表字段名
默认值:当前类名
Text N
type 字段类型 Text N
length 字段长度 Text N
not-null 字段是否允许为空 Bool N
unique 字段是否唯一(是否允许重复值) Bool N
insert Insert 操作时是否包含本字段数据
默认:true
Bool N
update Update操作时是否包含本字段数据
默认:true
Bool N
典型场景:
/**
* @hibernate.property
* column="name"
* length="50"
* not-null="true"
*
* @return String
*/
public String getName() {
return this.name;
}
注意:在编写代码的时候请,对将POJO的getter/setter方法设定为public,如果
设定为private,Hibernate将无法对属性的存取进行优化,只能转而采用传统的反射机制
进行操作,这将导致大量的性能开销(特别是在1.4之前的Sun JDK版本以及IBM JDK中,
反射所带来的系统开销相当可观)。
包含XDoclet Tag的代码必须由xdoclet程序进行处理以生成对应的映射文件,
xdoclet的处理模块可通过ant进行加载,下面是一个简单的hibernate xdoclet的ant
构建脚本(注意实际使用时需要根据实际情况对路径和CLASSPATH设定进行调整):
<?xml version="1.0"?>
<project name="Hibernate" default="hibernate" basedir=".">
<property name="xdoclet.lib.home"
value="C:\xdoclet-1.2.1\lib"/>
<target name="hibernate" depends=""
description="Generates Hibernate class descriptor files.">
<taskdef name="hibernatedoclet"
classname="xdoclet.modules.hibernate.HibernateDocletTask">
<classpath>
<fileset dir="${xdoclet.lib.home}">
<include name="*.jar"/>
</fileset>
</classpath>
</taskdef>
<hibernatedoclet
destdir="./src/"
excludedtags="@version,@author,@todo"
force="true"
verbose="true"
mergedir=".">
<fileset dir="./src/">
<include name="**/hibernate/sample/*.java"/>
</fileset>
<hibernate version="2.0"/>
</hibernatedoclet>
</target>
</project>
除了上面我们介绍的Hibernate Doclet Tag,其他还有:
Class层面;
@hibernate.cache
@hibernate.jcs-cache
@hibernate.joined-subclass
@hibernate.joined-subclass-key
@hibernate.query
Method层面
@hibernate.array
@hibernate.bag
@hibernate.collection-cache
@hibernate.collection-composite-element
@hibernate.collection-element
@hibernate.collection-index
@hibernate.collection-jcs-cache
@hibernate.collection-key
@hibernate.collection-key-column
@hibernate.collection-many-to-many
@hibernate.collection-one-to-many
@hibernate.column
@hibernate.component
@hibernate.generator-param
@hibernate.index-many-to-many
@hibernate.list
@hibernate.many-to-one
@hibernate.map
@hibernate.one-to-one
@hibernate.primitive-array
@hibernate.set
@hibernate.timestamp
@hibernate.version
具体的Tag描述请参见XDoclet官方网站提供的Tag说明1。下面的Hibernate高级特性介
绍中,我们也将涉及到这些Tag的实际使用。
1 http://xdoclet.sourceforge.net/xdoclet/tags/hibernate-tags.html
数据检索
数据查询与检索是Hibernate中的一个亮点。相对其他ORM实现而言,Hibernate
提供了灵活多样的查询机制。其中包括:
1. Criteria Query
2. Hibernate Query Language (HQL)
3. SQL
Criteria Query
Criteria Query通过面向对象化的设计,将数据查询条件封装为一个对象。简单来
讲,Criteria Query可以看作是传统SQL的对象化表示,如:
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
criteria.add(Expression.eq("sex",new Integer(1)));
这里的criteria 实例实际上是SQL “Select * from t_user where
name=’Erica’ and sex=1”的封装(我们可以打开Hibernate 的show_sql 选项,
以观察Hibernate在运行期生成的SQL语句)。
Hibernate 在运行期会根据Criteria 中指定的查询条件(也就是上面代码中通过
criteria.add方法添加的查询表达式)生成相应的SQL语句。
这种方式的特点是比较符合Java 程序员的编码习惯,并且具备清晰的可读性。正因
为此,不少ORM实现中都提供了类似的实现机制(如Apache OJB)。
对于Hibernate的初学者,特别是对SQL了解有限的程序员而言,Criteria Query
无疑是上手的极佳途径,相对HQL,Criteria Query提供了更易于理解的查询手段,借
助IDE的Coding Assist机制,Criteria的使用几乎不用太多的学习。
Criteria 查询表达式
Criteria 本身只是一个查询容器,具体的查询条件需要通过Criteria.add
方法添加到Criteria实例中。
如前例所示,Expression 对象具体描述了查询条件。针对SQL 语法,
Expression提供了对应的查询限定机制,包括:
方法 描述
Expression.eq 对应SQL“field = value”表达式。
如Expression.eq("name","Erica")
Expression.allEq 参数为一个Map对象,其中包含了多个属性-值对
应关系。相当于多个Expression.eq关系的叠加。
Expression.gt 对应SQL中的 “field > value ” 表达式
Expression.ge 对应SQL中的 “field >= value” 表达式
Expression.lt 对应SQL中的 “field < value” 表达式
Expression.le 对应SQL中的 “field <= value” 表达式
Expression.between 对应SQL中的 “between” 表达式
如下面的表达式表示年龄(age)位于13到50区
间内。
Expression.between("age",new
Integer(13),new Integer(50));
Expression.like 对应SQL中的 “field like value” 表达式
Expression.in 对应SQL中的 ”field in …” 表达式
Expression.eqProperty 用于比较两个属性之间的值,对应SQL中的“field
= field”。
如:
Expression.eqProperty(
"TUser.groupID",
"TGroup.id"
);
Expression.gtProperty 用于比较两个属性之间的值,对应SQL中的“field
> field”。
Expression.geProperty 用于比较两个属性之间的值,对应SQL中的“field
>= field”。
Expression.ltProperty 用于比较两个属性之间的值,对应SQL中的“field
< field”。
Expression.leProperty 用于比较两个属性之间的值,对应SQL中的“field
<= field”。
Expression.and and关系组合。
如:
Expression.and(
Expression.eq("name","Erica"),
Expression.eq(
"sex",
new Integer(1)
)
);
Expression.or or关系组合。
如:
Expression.or(
Expression.eq("name","Erica"),
Expression.eq("name","Emma")
);
Expression.sql 作为补充,本方法提供了原生SQL语法的支持。我
们可以通过这个方法直接通过SQL语句限定查询
条件。
下面的代码返回所有名称以“Erica”起始的记录:
Expression.sql(
“lower({alias}.name) like lower(?)”,
"Erica%",
Hibernate.STRING
);
其中的“{alias}”将由Hibernate在运行期使
用当前关联的POJO别名替换。
注意Expression 各方法中的属性名参数(如Express.eq中的第一个参数),这里
所谓属性名是POJO中对应实际库表字段的属性名(大小写敏感),而非库表中的实
际字段名称。
Criteria 高级特性
限定返回的记录范围
通过criteria. setFirstResult/setMaxResults 方法可以限制一次查询返回
的记录范围:
Criteria criteria = session.createCriteria(TUser.class);
//限定查询返回检索结果中,从第一百条结果开始的20条记录
criteria.setFirstResult(100);
criteria.setMaxResults(20);
对查询结果进行排序
//查询所有groupId=2的记录
//并分别按照姓名(顺序)和groupId(逆序)排序
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("groupId",new Integer(2)));
criteria.addOrder(Order.asc("name"));
criteria.addOrder(Order.desc("groupId"));
Criteria作为一种对象化的查询封装模式,不过由于Hibernate在实现过程中将精力
更加集中在HQL查询语言上,因此Criteria的功能实现还没做到尽善尽美(这点上,OJB
的Criteria 实现倒是值得借鉴),因此,在实际开发中,建议还是采用Hibernate 官
方推荐的查询封装模式:HQL。
Hibernate Query Language (HQL)
Criteria提供了更加符合面向对象编程模式的查询封装模式。不过,HQL(Hibernate
Query Language)提供了更加强大的功能,在官方开发手册中,也将HQL作为推荐的查询
模式。
相对Criteria,HQL提供了更接近传统SQL语句的查询语法,也提供了更全面的特性。
最简单的一个例子:
String hql = "from org.hibernate.sample.TUser";
Query query = session.createQuery(hql);
List userList = query.list();
上面的代码将取出TUser的所有对应记录。
如果我们需要取出名为“Erica”的用户的记录,类似SQL,我们可以通过SQL 语句加
以限定:
String hql =
"from org.hibernate.sample.TUser as user where user.name='Erica'";
Query query = session.createQuery(hql);
List userList = query.list();
其中我们新引入了两个子句“as”和“where”,as子句为类名创建了一个别名,而where
子句指定了限定条件。
HQL子句本身大小写无关,但是其中出现的类名和属性名必须注意大小写区分。
关于HQL,Hibernate 官方开发手册中已经提供了极其详尽的说明和示例,详见
Hibernate官方开发手册(Chapter 11)。
数据关联
一对一关联
配置:
Hibernate中的一对一关联由“one-to-one”节点定义。
在我们的权限管理系统示例中,每个用户都从属于一个用户组。如用户“Erica”
从属于“System Admin”组,从用户的角度出发,这就是一个典型的(单向)一对
一关系。
每个用户对应一个组,这在我们的系统中反映为TUser 到 TGroup 的
one-to-one 关系。其中TUser 是主控方,TGroup是被动方。
one-to-one关系定义比较简单,只需在主控方加以定义。这里,我们的目标是
由TUser 对象获取其对应的TGroup 对象。因此TUser 对象是主控方,为了实现一
对一关系,我们在TUser 对象的映射文件TUser.hbm.xml 中加入one-to-one节
点,对TGroup对象进行一对一关联:
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
>
……
<one-to-one
name="group"
class="org.hibernate.sample.TGroup"
cascade="none"
outer-join="auto"
constrained="false"
/>
……
</class>
</hibernate-mapping>
如果采用XDoclet,则对应的Tag如下:
/**
* @hibernate.class
* table="t_user"
* dynamic-update="true"
* dynamic-insert="true"
*
*/
public class TUser implements Serializable {
……
private TGroup group;
/**
* @hibernate.one-to-one
* name="group"
* cascade="none"
* class="org.hibernate.sample.TGroup"
* outer-join="auto"
* @return
*/
public TGroup getGroup() {
return group;
}
……
}
one-to-one 节点有以下属性:
属性 描述 类型 必须
name 映射属性 Text N
class 目标映射类。
注意要设为包含Package name的全路
径名称。
Text N
cascade 操作级联(cascade)关系。
可选值:
all : 所有情况下均进行级联操作。
none:所有情况下均不进行级联操作。
save-update:在执行save-update时
进行级联操作。
delete:在执行delete时进行级联操作。
级联(cascade)在Hibernate映射关
系中是个非常重要的概念。它指的是当主
控方执行操作时,关联对象(被动方)是
否同步执行同一操作。如对主控对象调用
save-update或delete方法时,是否同
时对关联对象(被动方)进行
Text N
save-update或delete。
这里,当用户(TUser)被更新或者删除
时,其所关联的组(TGroup)不应被修
改或者删除,因此,这里的级联关系设置
为none。
constrained 约束
表明主控表的主键上是否存在一个外键
(foreign key)对其进行约束。这个选
项关系到save、delete等方法的级联操
作顺序。
Bool N
outer-join 是否使用外联接。
true:总是使用outer-join
false:不使用outer-join
auto(默认) :如果关联对象没有采用
Proxy机制,则使用outer-join.
Text N
property-ref 关联类中用于与主控类相关联的属性名
称。
默认为关联类的主键属性名。
这里我们通过主键达成一对一的关联,所
以采用默认值即可。如果一对一的关联并
非建立在主键之间,则可通过此参数指定
关联属性。
Text N
access 属性值的读取方式。
可选项:
field
property(默认)
ClassName
Text N
一对多关联
一对多关系在系统实现中也很常见。典型的例子就是父亲与孩子的关系。 而在我
们现在的这个示例中,每个用户(TUser)都关联到多个地址(TAddress),如一个
用户可能拥有办公室地址、家庭地址等多个地址属性。这样,在系统中,就反应为一
个“一对多”关联。
一对多关系分为单向一对多关系和双向一对多关系。
单向一对多关系只需在“一”方进行配置,双向一对多关系需要在关联双方均加
以配置。
Ø 单向一对多关系
配置:
对于主控方(TUser):
TUser.hbm.xml:
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
>
……
<set
name="addresses"
table="t_address"
lazy="false"
inverse="false"
cascade="all"
sort="unsorted"
order-by="zipcode asc"
>
<key
column="user_id"
>
</key>
<one-to-many
class="org.hibernate.sample.TAddress"
/>
</set>
……
</class>
</hibernate-mapping>
对应的XDoclet Tag 如下:
/**
* @hibernate.collection-one-to-many
* class="org.hibernate.sample.TAddress"
*
* @hibernate.collection-key column="user_id"
*
* @hibernate.set
* name="addresses"
* table="t_address"
* inverse="false"
* cascade="all"
* lazy="false"
* sort=”unsorted”
* order-by="zipcode asc"
*
*/
public Set getAddresses() {
return addresses;
}
被动方(Taddress)的记录由Hibernate 负责读取,之后存放在主控方指定的
Collection类型属性中。
对于one-to-many 关联关系, 我们可以采用java.util.Set ( 或者
net.sf.hibernate.collection.Bag)类型的Collection,表现在XML 映射文件
中也就是<set>…</set>(或<bag>…</bag>)节点。关于Hibernate的Collection
实现,请参见Hibernate Reference.
one-to-many 节点有以下属性:
属性 描述 类型 必须
name 映射属性 Text Y
table 目标关联数据库表。 Text Y
lazy 是否采用延迟加载。
关于延迟加载,请参见后面相关章节。
Text N
inverse 用于标识双向关联中的被动方一端。
inverse=false的一方(主控方)负责
维护关联关系。
默认值: false
Bool N
cascade 操作级联(cascade)关系。
可选值:
all : 所有情况下均进行级联操作。
none:所有情况下均不进行级联操作。
save-update:在执行save-update时
进行级联操作。
delete:在执行delete时进行级联操作。
Text N
sort 排序类型。 Text N
可选值:
unsorted :不排序(默认)
natural :自然顺序(避免与order-by
搭配使用)
comparatorClass :指以某个实现了
java.util.Comparator接口的类作为排
序算法。
order-by 指定排序字段及其排序方式。
(JDK1.4以上版本有效)。
对应SQL中的order by子句。
避免与sort 的 “natural”模式同时使
用。
Text N
where 数据甄选条件,如果只需要处理库表中某
些特定数据的时候,可通过此选项设定结
果集限定条件。
Text N
outer-join 是否使用外联接。
true:总是使用outer-join
false:不使用outer-join
auto(默认) :如果关联对象没有采用
Proxy机制,则使用outer-join.
Text N
batch-size 采用延迟加载特性时(Lazy Loading)
一次读入的数据数量。
此处未采用延迟加载机制,因此此属性忽
略。
Int N
access 属性值的读取方式。
可选项:
field
property(默认)
ClassName
Text N
通过单向一对多关系进行关联相对简单,但是存在一个问题。由于是单向关联,
为了保持关联关系,我们只能通过主控方对被动方进行级联更新。且如果被关联方的
关联字段为“NOT NULL”,当Hibernate创建或者更新关联关系时,还可能出现约
束违例。
例如我们想为一个已有的用户“Erica”添加一个地址对象:
Transaction tx = session.beginTransaction();
TAddress addr = new TAddress();
addr.setTel("1123");
addr.setZipcode("233123");
addr.setAddress("Hongkong");
user.getAddresses().add(addr);
session.save(user);//通过主控对象级联更新
tx.commit();
为了完成这个操作,Hibernate会分两步(两条SQL)来完成新增t_address
记录的操作:
1. save(user)时:
insert into t_address (user_id, address, zipcode, tel)
values (null, "Hongkong", "233123", "1123")
2. tx.commit()时
update t_address set user_id=”1”, address="Hongkong",
zipcode="233123", tel="1123" where id=2
第一条SQL用于插入新的地址记录。
第二条SQL用于更新t_address,将user_id设置为其关联的user对象的id值。
问题就出在这里,数据库中,我们的t_address.user_id字段为“NOT NULL”
型,当Hibernate执行第一条语句创建t_address记录时,试图将user_id字段的
值设为null,于是引发了一个约束违例异常:
net.sf.hibernate.PropertyValueException: not-null property
references a null or transient value:
org.hibernate.sample.TAddress.userId
因为关联方向是单向,关联关系由TUser对象维持,而被关联的addr对象本身并
不知道自己与哪个TUser对象相关联,也就是说,addr对象本身并不知道user_id应
该设为什么数值。
因此,在保存addr时,只能先在关联字段插入一个空值。之后,再由TUser对象
将自身的id值赋予关联字段addr.user_id,这个赋值操作导致addr对象属性发生变
动,在事务提交时,hibernate会发现这一改变,并通过update sql将变动后的数
据保存到数据库。
第一个步骤中,企图向数据库的非空字段插入空值,因此导致了约束违例。
既然TUser对象是主控方,为什么就不能自动先设置好下面的TAddress对象的
关俩字段值再一次做Insert操作呢?莫名其妙?Ha,don’t ask me ,go to ask
Hibernate TeamJ。
我们可以在设计的时候通过一些手段进行调整,以避免这样的约束违例,如将关
联字段设为允许NULL值、直接采用数值型字段作为关联(有的时候这样的调整并不可
行,很多情况下我们必须针对现有数据库结构进行开发),或者手动为关联字段属性
赋一个任意非空值(即使在这里通过手工设置了正确的user_id也没有意义,
hibernate还是会自动再调用一条Update语句进行更新)。
甚至我们可以将被动方的关联字段从其映射文件中剔除(如将user_id字段的映
射从TAddress.hbm.xml中剔除)。这样Hibernate在生成第一条insert语句的时
候就不会包含这个字段(数据库会使用字段默认值填充),如:之后update语句会根
据主控方的one-to-many映射配置中的关联字段去更新被动方关联字段的内容。在我
们这里的例子中,如果将user_id字段从TAddress.hbm.xml文件中剔除,
Hibernate在保存数据时会生成下面几条SQL:
1. insert into t_address (address, zipcode, tel) values
('Hongkong', '233123', '1123')
2. update t_address set user_id=1 where id=7
生成第一条insert语句时,没有包含user_id字段,数据库会使用该字段的默
认值(如果有的话)进行填充。因此不会引发约束违例。之后,根据第一条语句返回
的记录id,再通过update语句对user_id字段进行更新。
但是,纵使采用这些权益之计,由于Hibernate实现机制中,采用了两条SQL进
行一次数据插入操作,相对单条insert,几乎是两倍的性能开销,效率较低,因此,
对于性能敏感的系统而言,这样的解决方案所带来的开销可能难以承受。
针对上面的情况,我们想到,如果addr对象知道如何获取user_id字段的内容,
那么执行insert语句的时候直接将数据植入即可。这样不但绕开了约束违例的可能,
而且还节省了一条Update语句的开销,大幅度提高了性能。
双向一对多关系的出现则解决了这个问题。它除了避免约束违例和提高性能的好
处之外,还带来另外一个优点,由于建立了双向关联,我们可以在关联双方中任意一
方,访问关联的另一方(如可以通过TAddress对象直接访问其关联的TUser对象),
这提供了更丰富灵活的控制手段。
Ø 双向一对多关系
双向一对多关系,实际上是“单向一对多关系”与“多对一关系”的组合。也就
是说我们必须在主控方配置单向一对多关系的基础上,在被控方配置多对一关系与其
对应。
配置:
上面我们已经大致完成了单向方一对多关系的配置,我们只需在此基础上稍做修
改,并对(t_address)的相关属性进行配置即可:
TUser.hbm.xml:
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
>
……
<set
name="addresses"
table="t_address"
lazy="false"
inverse="true" ①
cascade="all"
sort="unsorted"
order-by="zipcode asc"
>
<key
column="user_id"
>
</key>
<one-to-many
class="org.hibernate.sample.TAddress"
/>
</set>
</class>
</hibernate-mapping>
① 这里与前面不同,inverse被设为“true”,这意味着TUser不再作为主控方,
而是将关联关系的维护工作交给关联对象org.hibernate.sample.TAddress 来
完成。这样TAddress对象在持久化过程中,就可以主动获取其关联的TUser对象的id,
并将其作为自己的user_id,之后执行一次insert操作即可完成全部工作。
在one-to-many 关系中,将many 一方设为主动方(inverse=false)将有助性能
的改善。(现实中也一样,如果要让胡锦涛记住全国人民的名字,估计花个几十年也
不可能,但要让全国人民知道胡锦涛,可就不需要那么多时间了。J)
对应的 xdoclet tag 如下:
public class TUser implements Serializable {
……
private Set addresses = new HashSet();
……
/**
* @hibernate.collection-one-to-many
* class="org.hibernate.sample.TAddress"
*
* @hibernate.collection-key column="user_id"
*
* @hibernate.set
* name="addresses"
* table="t_address"
* inverse="true"
* lazy="false"
* cascade=”all”
* sort="unsorted"
* order-by="zipcode asc"
*/
public Set getAddresses() {
return addresses;
}
……
}
TAddress.hbm.xml:
<hibernate-mapping>
<class
name="org.hibernate.sample.TAddress"
table="t_address"
dynamic-update="false"
dynamic-insert="false"
>
……
<many-to-one
name="user" ①
class="org.hibernate.sample.TUser"
cascade="none"
outer-join="auto"
update="true"
insert="true"
access="property"
column="user_id"
not-null="true"
/>
</class>
</hibernate-mapping>
① 在TAddress 对象中新增一个TUser field “user”,并为其添加对应的
getter/setter 方法。同时删除原有的user_id 属性及其映射配置,否则运行期会报
字段重复映射错误:“Repeated column in mapping”。
对应Xdoclet tag:
public class TAddress implements Serializable {
……
private TUser user;
……
/**
* @hibernate.many-to-one
* name="user"
* column="user_id"
* not-null="true"
*
*/
public TUser getUser() {
return this.user;
}
……
}
再看上面那段代码片断:
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
List userList = criteria.list();
TUser user =(TUser)userList.get(0);
Transaction tx = session.beginTransaction();
TAddress addr = new TAddress();
addr.setTel("1123");
addr.setZipcode("233123");
addr.setAddress("Hongkong");
user.getAddresses().add(addr);
session.save(user);//通过主控对象级联更新
tx.commit();
尝试运行这段代码,结果凄凉的很,还是约束违例。
为什么会这样,我们已经配置了TAddress的many-to-one关系,这么看来似
乎没什么效果……
不过,别忘了上面提到的inverse 属性,这里我们把TUser 的inverse 设为
“true”,即指定由对方维护关联关系,在这里也就是由TAddress维护关联关系。
TUser既然不再维护关联关系,那么TAddress的user_id属性它也自然不会关心,
必须由TAddress自己去维护user_id:
……
TAddress addr = new TAddress();
addr.setTel("1123");
addr.setZipcode("233123");
addr.setAddress("Hongkong");
addr.setUser(user);//设置关联的TUser对象
user.getAddresses().add(addr);
session.save(user);//级联更新
……
观察Hibernate执行过程中调用的SQL语句:
insert into t_address (user_id, address, zipcode, tel) values
(1, 'Hongkong', '233123', '1123')
正如我们所期望的,保存工作通过单条Insert语句的执行来完成。
many-to-one 节点有以下属性:
属性 描述 类型 必须
name 映射属性 Text Y
column 关联字段。 Text N
class 类名
默认为映射属性所属类型
Text N
cascade 操作级联(cascade)关系。
可选值:
all : 所有情况下均进行级联操作。
none:所有情况下均不进行级联操作。
save-update:在执行save-update时
进行级联操作。
delete:在执行delete时进行级联操作。
Text N
update 是否对关联字段进行Update操作 Bool N
默认:true
insert 是否对关联字段进行Insert操作
默认:true
Bool N
outer-join 是否使用外联接。
true:总是使用outer-join
false:不使用outer-join
auto(默认) :如果关联对象没有采用
Proxy机制,则使用outer-join.
Text N
property-ref 用于与主控类相关联的属性的名称。
默认为关联类的主键属性名。
这里我们通过主键进行关联,所以采用默
认值即可。如果关联并非建立在主键之
间,则可通过此参数指定关联属性。
Text N
access 属性值的读取方式。
可选项:
field
property(默认)
ClassName
Text N
级联与关联关系的差别?
多对多关联
Hibernate关联关系中相对比较特殊的就是多对多关联,多对多关联与一对一关
联和一对多关联不同,多对多关联需要另外一张映射表用于保存多对多映射信息。
由于多对多关联的性能不佳(由于引入了中间表,一次读取操作需要反复数次查
询),因此在设计中应该避免大量使用。同时,在对多对关系中,应根据情况,采取
延迟加载(Lazy Loading 参见后续章节)机制来避免无谓的性能开销。
在一个权限管理系统中,一个常见的多对多的映射关系就是Group 与Role,以
及Role与Privilege之间的映射。
Ø Group代表“组”(如“业务主管”);
Ø Role代表“角色”(如“出纳”、“财务”);
Ø Privilege 代表某个特定资源的访问权限(如“修改财务报表”,“查询
财务报表”)。
这里我们以Group和Role之间的映射为例:
Ø 一个Group中包含了多个Role,如某个“业务主管”拥有“出纳”和“财
务”的双重角色。
Ø 而一个Role也可以属于不同的Group。
配置:
在我们的实例中,TRole 和TPrivilege 对应数据库中的t_role、
t_privilege表。
TGroup.hbm.xml中关于多对多关联的配置片断:
<hibernate-mapping>
<class
name="org.hibernate.sample.TGroup"
table="t_group"
dynamic-update="false"
dynamic-insert="false"
>
……
<set
name="roles"
table="t_group_role" ①
lazy="false"
inverse="false"
cascade="save-update" ②
>
<key
column="group_id" ③
>
</key>
<many-to-many
class="org.hibernate.sample.TRole"
column="role_id" ④
/>
</set>
</class>
</hibernate-mapping>
① 这里为t_group 和t_role之间的映射表。
② 一般情况下,cascade应该设置为“save-update”,对于多对多逻辑
而言,很少出现删除一方需要级联删除所有关联数据的情况,如删除一
个Group,一般不会删除其中包含的Role(这些Role 可能还被其他的
Group所引用)。反之删除Role一般也不会删除其所关联的所有Group。
③ 映射表中对于t_group表记录的标识字段。
④ 映射表中对于t_role表记录的标识字段。
对应的xdoclet tag如下:
public class TGroup implements Serializable {
……
private Set roles = new HashSet();
/**
* @hibernate.set
* name="roles"
* table="t_group_role"
* lazy="false"
* inverse="false"
* cascade="save-update"
* sort=”unsorted”
*
* @hibernate.collection-key
* column="group_id"
*
* @hibernate.collection-many-to-many
* class="org.hibernate.sample.TRole"
* column="role_id"
*
*/
public Set getRoles() {
return roles;
}
……
}
TRole.hbm.xml中关于多对多关联的配置片断:
<hibernate-mapping>
<class
name="org.hibernate.sample.TRole"
table="t_role"
dynamic-update="false"
dynamic-insert="false"
>
……
<set
name="groups"
table="t_group_role"
lazy="false"
inverse="true"
cascade="save-update"
sort="unsorted"
>
<key
column="role_id"
>
</key>
<many-to-many
class="org.hibernate.sample.TGroup"
column="group_id"
outer-join="auto"
/>
</set>
</class>
</hibernate-mapping>
对应的xdoclet如下:
public class TRole implements Serializable {
private Set groups = new HashSet();
……
/**
*
* @hibernate.set
* name="groups"
* table="t_group_role"
* cascade="save-update"
* inverse="true"
* lazy="false"
*
* @hibernate.collection-key
* column="role_id"
*
* @hibernate.collection-many-to-many
* class="org.hibernate.sample.TGroup"
* column="group_id"
*
*
*/
public Set getGroups() {
return groups;
}
}
many-to-many节点中各个属性描述:
属性 描述 类型 必须
column 中间映射表中,关联目标表的关联字段。 Text Y
class 类名
关联目标类。
Text Y
outer-join 是否使用外联接。
true:总是使用outer-join
false:不使用outer-join
auto(默认) :如果关联对象没有采用
Proxy机制,则使用outer-join.
Text N
使用:
多对多关系中,由于关联关系是两张表相互引用,因此在保存关联状态时必须对
双方同时保存。
public void testPersist(){
TRole role1 = new TRole();
role1.setName("Role1");
TRole role2 = new TRole();
role2.setName("Role2");
TRole role3 = new TRole();
role3.setName("Role3");
TGroup group1 = new TGroup();
group1.setName("group1");
TGroup group2 = new TGroup();
group2.setName("group2");
TGroup group3 = new TGroup();
group3.setName("group3");
group1.getRoles().add(role1);
group1.getRoles().add(role2);
group2.getRoles().add(role2);
group2.getRoles().add(role3);
group3.getRoles().add(role1);
group3.getRoles().add(role3);
role1.getGroups().add(group1);
role1.getGroups().add(group3);
role2.getGroups().add(group1);
role2.getGroups().add(group2);
role3.getGroups().add(group2);
role3.getGroups().add(group3);
try {
Transaction tx = session.beginTransaction();
//多对多关系必须同时对关联双方进行保存
session.save(role1);
session.save(role2);
session.save(role3);
session.save(group1);
session.save(group2);
session.save(group3);
tx.commit();
} catch (Exception e) {
e.printStackTrace();
Assert.fail(e.getMessage());
}
}
上面的代码创建3个TGroup对象和3个TRole对象,并形成了多对多关系。
数据访问
PO和VO
PO即 Persistence Object
VO即 Value Object
PO和VO是Hibernate中两个比较关键的概念。
首先,何谓VO,很简单,VO就是一个简单的值对象。
如:
TUser user = new TUser();
user.setName("Emma");
这里的user就是一个VO。VO只是简单携带了对象的一些属性信息。
何谓PO? 即纳入Hibernate管理框架中的VO。看下面两个例子:
TUser user = new TUser();
TUser anotherUser = new TUser();
user.setName("Emma");
anotherUser.setName("Kevin");
//此时user和anotherUser都是VO
Transaction tx = session.beginTransaction();
session.save(user);
//此时的user已经经过Hibernate的处理,成为一个PO
//而anotherUser仍然是个VO
tx.commit();
//事务提交之后,库表中已经插入一条用户”Emma”的记录
//对于anotherUser则无任何操作
Transaction tx = session.beginTransaction();
user.setName("Emma_1"); //PO
anotherUser.setName("Kevin_1");//VO
tx.commit();
//事务提交之后,PO的状态被固化到数据库中
//也就是说数据库中“Emma”的用户记录已经被更新为“Emma_1”
//此时anotherUser仍然是个普通Java对象,它的属性更改不会
//对数据库产生任何影响
另外,通过Hibernate返回的对象也是PO:
//由Hibernate返回的PO
TUser user = (TUser)session.load(TUser.class,new Integer(1));
VO经过Hibernate进行处理,就变成了PO。
上面的示例代码session.save(user)中,我们把一个VO “user”传递给
Hibernate的Session.save方法进行保存。在save方法中,Hibernate对其进
行如下处理:
1. 在当前session所对应的实体容器(Entity Map)中查询是否存在user对象
的引用。
2. 如果引用存在,则直接返回user对象id,save过程结束.
Hibernate中,针对每个Session有一个实体容器(实际上是一个Map对象),
如果此容器中已经保存了目标对象的引用,那么hibernate会认为此对象已经
与Session相关联。
对于save操作而言,如果对象已经与Session相关联(即已经被加入Session
的实体容器中),则无需进行具体的操作。因为之后的Session.flush过程中,
Hibernate会对此实体容器中的对象进行遍历,查找出发生变化的实体,生成
并执行相应的update语句。
3. 如果引用不存在,则根据映射关系,执行insert操作。
a) 在我们这里的示例中,采用了native的id生成机制,因此hibernate会
从数据库取得insert操作生成的id并赋予user对象的id属性。
b) 将user对象的引用纳入Hibernate的实体容器。
c) save过程结束,返回对象id.
而Session.load方法中,再返回对象之前,Hibernate就已经将此对象纳入其实
体容器中。
VO和PO的主要区别在于:
Ø VO是独立的Java Object。
Ø PO是由Hibernate纳入其实体容器(Entity Map)的对象,它代表了与数
据库中某条记录对应的Hibernate实体,PO的变化在事务提交时将反应到实
际数据库中。
如果一个PO与Session对应的实体容器中分离(如Session关闭后的PO),那么
此时,它又会变成一个VO。
由PO、VO的概念,又引申出一些系统层次设计方面的问题。如在传统的MVC架构中,
位于Model层的PO,是否允许被传递到其他层面。由于PO的更新最终将被映射到实
际数据库中,如果PO在其他层面(如View层)发生了变动,那么可能会对Model
层造成意想不到的破坏。
因此,一般而言,应该避免直接PO传递到系统中的其他层面,一种解决办法是,通
过一个VO,通过属性复制使其具备与PO相同属性值,并以其为传输媒质(实际上,
这个VO被用作Data Transfer Object,即所谓的DTO),将此VO传递给其他层
面以实现必须的数据传送。
属性复制可以通过Apache Jakarta Commons Beanutils
(http://jakarta.apache.org/commons/beanutils/)组件提供的属性批
量复制功能,避免繁复的get/set操作。
下面的例子中,我们把user对象的所有属性复制到anotherUser对象中:
TUser user = new TUser();
TUser anotherUser = new TUser();
user.setName("Emma");
user.setUserType(1);
try {
BeanUtils.copyProperties(anotherUser,user);
System.out.println("UserName => "
+anotherUser.getName()
);
System.out.println("UserType => "
+ anotherUser.getUserType()
);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
关于unsaved-value
在非显示数据保存时,Hibernate将根据这个值来判断对象是否需要保存。
所谓显式保存,是指代码中明确调用session 的save、update、saveOrupdate方
法对对象进行持久化。如:
session.save(user);
而在某些情况下,如映射关系中,Hibernate 根据级联(Cascade)关系对联接类进
行保存。此时代码中没有针对级联对象的显示保存语句,需要Hibernate 根据对象当前状
态判断是否需要保存到数据库。此时,Hibernate即将根据unsaved-value进行判定。
首先Hibernate会取出目标对象的id。
之后,将此值与unsaved-value进行比对,如果相等,则认为目标对象尚未保存,否
则,认为对象已经保存,无需再进行保存操作。
如:user对象是之前由hibernate从数据库中获取,同时,此user对象的若干个关
联对象address 也被加载,此时我们向user 对象新增一个address 对象,此时调用
session.save(user),hibernate会根据unsaved-value判断user对象的数个address
关联对象中,哪些需要执行save操作,而哪些不需要。
对于我们新加入的address 对象而言,由于其id(Integer 型)尚未赋值,因此为
null,与我们设定的unsaved-value(null)相同,因此hibernate将其视为一个未保存
对象,将为其生成insert语句并执行。
这里可能会产生一个疑问,如果“原有”关联对象发生变动(如user的某个“原有”
的address对象的属性发生了变化,所谓“原有”即此address对象已经与user相关联,
而不是我们在此过程中为之新增的),此时id值是从数据库中读出,并没有发生改变,自然
与unsaved-value(null)也不一样,那么Hibernate是不是就不保存了?
上面关于PO、VO 的讨论中曾经涉及到数据保存的问题,实际上,这里的“保存”,
实际上是“insert”的概念,只是针对新关联对象的加入,而非数据库中原有关联对象的
“update”。所谓新关联对象,一般情况下可以理解为未与Session 发生关联的VO。而
“原有”关联对象,则是PO。如上面关于PO、VO的讨论中所述:
对于save操作而言,如果对象已经与Session相关联(即已经被加入Session的实体
容器中),则无需进行具体的操作。因为之后的Session.flush过程中,Hibernate
会对此实体容器中的对象进行遍历,查找出发生变化的实体,生成并执行相应的update
语句。
Inverse和Cascade
Inverse,直译为“反转”。在Hibernate语义中,Inverse指定了关联关系中的
方向。
关联关系中,inverse=”false”的为主动方,由主动方负责维护关联关系。具体可
参见一对多关系中的描述。
而Cascade,译为“级联”,表明对象的级联关系,如TUser的Cascade设为all,
就表明如果发生对user对象的操作,需要对user所关联的对象也进行同样的操作。如对
user对象执行save操作,则必须对user对象相关联的address也执行save操作。
初学者常常混淆inverse和cascade,实际上,这是两个互不相关的概念。Inverse
指的是关联关系的控制方向,而cascade指的是层级之间的连锁操作。
延迟加载(Lazy Loading)
为了避免一些情况下,关联关系所带来的无谓的性能开销。Hibernate引入了延迟加载的
概念。
如,示例中user对象在加载的时候,会同时读取其所关联的多个地址(address)对象,
对于需要对address进行操作的应用逻辑而言,关联数据的自动加载机制的确非常有效。
但是,如果我们只是想要获得user的性别(sex)属性,而不关心user的地址(address)
信息,那么自动加载address的特性就显得多余,并且造成了极大的性能浪费。为了获得user
的性别属性,我们可能还要同时从数据库中读取数条无用的地址数据,这导致了大量无谓的系统
开销。
延迟加载特性的出现,正是为了解决这个问题。
所谓延迟加载,就是在需要数据的时候,才真正执行数据加载操作。
对于我们这里的user对象的加载过程,也就意味着,加载user对象时只针对其本身的属性,
而当我们需要获取user对象所关联的address信息时(如执行user.getAddresses时),才
真正从数据库中加载address数据并返回。
我们将前面一对多关系中的lazy属性修改为true,即指定了关联对象采用延迟加载:
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
>
……
<set
name="addresses"
table="t_address"
lazy="true" ★
inverse="false"
cascade="all"
sort="unsorted"
order-by="zipcode asc"
>
<key
column="user_id"
>
</key>
<one-to-many
class="org.hibernate.sample.TAddress"
/>
</set>
……
</class>
</hibernate-mapping>
尝试执行以下代码:
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
List userList = criteria.list();
TUser user =(TUser)userList.get(0);
System.out.println("User name => "+user.getName());
Set hset = user.getAddresses();
session.close();//关闭Session
TAddress addr = (TAddress)hset.toArray()[0];
System.out.println(addr.getAddress());
运行时抛出异常:
LazyInitializationException - Failed to lazily initialize a
collection - no session or session was closed
如果我们稍做调整,将session.close放在代码末尾,则不会发生这样的问题。
这意味着,只有我们实际加载user关联的address时,Hibernate才试图通过
session从数据库中加载实际的数据集,而由于我们读取address之前已经关闭了
session,所以报出session已关闭的错误。
这里有个问题,如果我们采用了延迟加载机制,但希望在一些情况下,实现非延迟加
载时的功能,也就是说,我们希望在Session关闭后,依然允许操作user的addresses
属性。如,为了向View层提供数据,我们必须提供一个完整的User对象,包含其所关联的
address信息,而这个User对象必须在Session关闭之后仍然可以使用。
Hibernate.initialize方法可以通过强制加载关联对象实现这一功能:
Hibernate.initialize(user.getAddresses());
session.close();
//通过Hibernate.initialize方法强制读取数据
//addresses对象即可脱离session进行操作
Set hset= user.getAddresses();
TAddress addr = (TAddress)hset.toArray()[0];
System.out.println(addr.getAddress());
为了实现透明化的延迟加载机制,hibernate进行了大量努力。其中包括JDK
Collection接口的独立实现。
如果我们尝试用HashSet强行转化Hibernate返回的Set型对象:
Set hset = (HashSet)user.getAddresses();
就会在运行期得到一个java.lang.ClassCastException,实际上,此时返回的是
一个Hibernate的特定Set实现“net.sf.hibernate.collection.Set”对象,而非
传统意义上的JDK Set实现。
这也正是我们为什么在编写POJO时,必须用JDK Collection接口(如Set,Map),
而非特定的JDK Collection实现类(如HashSet、HashMap)申明Collection属性的
原因。
回到前面TUser类的定义:
public class TUser implements Serializable {
……
private Set addresses = new HashSet();
……
}
我们通过Set接口,申明了一个addresses属性,并创建了一个HashSet作为
addresses的初始实例,以便我们创建TUser实例后,就可以为其添加关联的address对
象:
TUser user = new TUser();
TAddress addr = new TAddress();
addr.setAddress("Hongkong");
user.getAddresses().add(addr);
session.save(user);
此时,这里的addresses属性还是一个HashSet对象,其中包含了一个address对象
的引用。那么,当调用session.save(user)时,Hibernate是如何处理这个HashSet
型属性的呢?
通过Eclipse的Debug窗口,我们可以看到session.save方法执行前后user对象发
生的变化:
图一 session.save方法之前的user对象
图二 session.save方法之后的user对象
可以看到,user对象在通过Hibernate处理之后已经发生了变化。
首先,由于insert操作,Hibernate获得数据库产生的id值(在我们的例子中,采
用native方式的主键生成机制),并填充到user对象的id属性。这个变化比较容易理解。
另一方面,Hibernate使用了自己的Collection实现
“net.sf.hibernate.collection.Set”对user中的HashSet型addresses属性进
行了替换,并用数据对其进行填充,保证新的addresses与原有的addresses包含同样的
实体元素。
由于拥有自身的Collection实现,Hibernate就可以在Collection层从容的实现
延迟加载特性。只有程序真正读取这个Collection时,才激发底层实际的数据库操作。
事务管理
Hibernate 是JDBC 的轻量级封装,本身并不具备事务管理能力。在事务管理层,
Hibernate将其委托给底层的JDBC或者JTA,以实现事务管理和调度功能。
Hibernate的默认事务处理机制基于JDBC Transaction。我们也可以通过配置文
件设定采用JTA作为事务管理实现:
<hibernate-configuration>
<session-factory>
……
<property name="hibernate.transaction.factory_class">
net.sf.hibernate.transaction.JTATransactionFactory
<!--net.sf.hibernate.transaction.JDBCTransactionFactory-->
</property>
……
</session-factory>
</hibernate-configuration>
基于JDBC的事务管理:
将事务管理委托给JDBC 进行处理无疑是最简单的实现方式,Hibernate 对于JDBC
事务的封装也极为简单。
我们来看下面这段代码:
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
……
tx.commit();
从JDBC层面而言,上面的代码实际上对应着:
Connection dbconn = getConnection();
dbconn.setAutoCommit(false);
……
dbconn.commit();
就是这么简单,Hibernate并没有做更多的事情(实际上也没法做更多的事情),只
是将这样的JDBC代码进行了封装而已。
这里要注意的是,在sessionFactory.openSession()中,hibernate会初始化
数据库连接,与此同时,将其AutoCommit 设为关闭状态(false)。而其后,在
Session.beginTransaction 方法中,Hibernate 会再次确认Connection 的
AutoCommit 属性被设为关闭状态( 为了防止用户代码对session 的
Connection.AutoCommit属性进行修改)。
这也就是说,我们一开始从SessionFactory获得的session,其自动提交属性就
已经被关闭(AutoCommit=false),下面的代码将不会对数据库产生任何效果:
session = sessionFactory.openSession();
session.save(user);
session.close();
这实际上相当于 JDBC Connection的AutoCommit属性被设为false,执行了若
干JDBC操作之后,没有调用commit操作即将Connection关闭。
如果要使代码真正作用到数据库,我们必须显式的调用Transaction指令:
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
基于JTA的事务管理:
JTA 提供了跨Session 的事务管理能力。这一点是与JDBC Transaction 最大的
差异。
JDBC事务由Connnection管理,也就是说,事务管理实际上是在JDBC Connection
中实现。事务周期限于Connection的生命周期之类。同样,对于基于JDBC Transaction
的Hibernate 事务管理机制而言,事务管理在Session 所依托的JDBC Connection
中实现,事务周期限于Session的生命周期。
JTA 事务管理则由 JTA 容器实现,JTA 容器对当前加入事务的众多Connection 进
行调度,实现其事务性要求。JTA的事务周期可横跨多个JDBC Connection生命周期。
同样对于基于JTA事务的Hibernate而言,JTA事务横跨可横跨多个Session。
下面这幅图形象的说明了这个问题:
图中描述的是JDBC Connection 与事务之间的关系,而Hibernate Session 在
这里与JDBC Connection具备同等的逻辑含义。
从上图中我们可以看出,JTA 事务是由JTA Container 维护,而参与事务的
Connection无需对事务管理进行干涉。这也就是说,如果采用JTA Transaction,我
们不应该再调用Hibernate的Transaction功能。
上面基于JDBC Transaction的正确代码,这里就会产生问题:
public class ClassA{
public void saveUser(User user){
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(user);
tx.commit();
session.close();
}
}
public class ClassB{
public void saveOrder(Order order){
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
session.save(order);
tx.commit();
session.close();
}
}
public class ClassC{
public void save(){
……
UserTransaction tx = new InitialContext().lookup(“……”);
ClassA.save(user);
ClassB.save(order);
tx.commit();
……
}
}
这里有两个类ClassA和ClassB,分别提供了两个方法:saveUser和saveOrder,
用于保存用户信息和订单信息。在ClassC中,我们接连调用了ClassA.saveUser方法
和ClassB.saveOrder 方法,同时引入了JTA 中的UserTransaction 以实现
ClassC.save方法中的事务性。
问题出现了,ClassA 和ClassB 中分别都调用了Hibernate 的Transaction 功
能。在Hibernate 的JTA 封装中,Session.beginTransaction 同样也执行了
InitialContext.lookup方法获取UserTransaction实例,Transaction.commit
方法同样也调用了UserTransaction.commit方法。实际上,这就形成了两个嵌套式的
JTA Transaction:ClassC 申明了一个事务,而在ClassC 事务周期内,ClassA 和
ClassB也企图申明自己的事务,这将导致运行期错误。
因此,如果决定采用JTA Transaction,应避免再重复调用Hibernate 的
Transaction功能,上面的代码修改如下:
public class ClassA{
public void save(TUser user){
session = sessionFactory.openSession();
session.save(user);
session.close();
}
……
}
public class ClassB{
public void save (Order order){
session = sessionFactory.openSession();
session.save(order);
session.close();
}
……
}
public class ClassC{
public void save(){
……
UserTransaction tx = new InitialContext().lookup(“……”);
classA.save(user);
classB.save(order);
tx.commit();
……
}
}
上面代码中的ClassC.save方法,也可以改成这样:
public class ClassC{
public void save(){
……
session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
classA.save(user);
classB.save(order);
tx.commit();
……
}
}
实际上,这是利用Hibernate来完成启动和提交UserTransaction的功能,但这
样的做法比原本直接通过InitialContext获取UserTransaction 的做法消耗了更多
的资源,得不偿失。
在EJB 中使用JTA Transaction 无疑最为简便,我们只需要将save 方法配置为
JTA事务支持即可,无需显式申明任何事务,下面是一个Session Bean的save方法,
它的事务属性被申明为“Required”,EJB容器将自动维护此方法执行过程中的事务:
/**
* @ejb.interface-method
* view-type="remote"
*
* @ejb.transaction type = "Required"
**/
public void save(){
//EJB环境中,通过部署配置即可实现事务申明,而无需显式调用事务
classA.save(user);
classB.save(log);
}//方法结束时,如果没有异常发生,则事务由EJB容器自动提交。
锁(locking)
业务逻辑的实现过程中,往往需要保证数据访问的排他性。如在金融系统的日终结算
处理中,我们希望针对某个cut-off时间点的数据进行处理,而不希望在结算进行过程中
(可能是几秒种,也可能是几个小时),数据再发生变化。此时,我们就需要通过一些机
制来保证这些数据在某个操作过程中不会被外界修改,这样的机制,在这里,也就是所谓
的“锁”,即给我们选定的目标数据上锁,使其无法被其他程序修改。
Hibernate支持两种锁机制:即通常所说的“悲观锁(Pessimistic Locking)”
和“乐观锁(Optimistic Locking)”。
悲观锁(Pessimistic Locking)
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自
外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定
状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能
真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系
统不会修改数据)。
一个典型的倚赖数据库的悲观锁调用:
select * from account where name=”Erica” for update
这条sql 语句锁定了account 表中所有符合检索条件(name=”Erica”)的记录。
本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
Hibernate的悲观锁,也是基于数据库的锁机制实现。
下面的代码实现了对查询记录的加锁:
String hqlStr =
"from TUser as user where user.name='Erica'";
Query query = session.createQuery(hqlStr);
query.setLockMode("user",LockMode.UPGRADE); //加锁
List userList = query.list();//执行查询,获取数据
query.setLockMode对查询语句中,特定别名所对应的记录进行加锁(我们为
TUser类指定了一个别名“user”),这里也就是对返回的所有user记录进行加锁。
观察运行期Hibernate生成的SQL语句:
select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id
as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex
from t_user tuser0_ where (tuser0_.name='Erica' ) for update
这里Hibernate通过使用数据库的for update子句实现了悲观锁机制。
Hibernate的加锁模式有:
Ø LockMode.NONE : 无锁机制。
Ø LockMode.WRITE :Hibernate在Insert和Update记录的时候会自动
获取。
Ø LockMode.READ : Hibernate在读取记录的时候会自动获取。
以上这三种锁机制一般由Hibernate内部使用,如Hibernate为了保证Update
过程中对象不会被外界修改,会在save方法实现中自动为目标对象加上WRITE锁。
Ø LockMode.UPGRADE :利用数据库的for update子句加锁。
Ø LockMode. UPGRADE_NOWAIT :Oracle的特定实现,利用Oracle的for
update nowait子句实现加锁。
上面这两种锁机制是我们在应用层较为常用的,加锁一般通过以下方法实现:
Criteria.setLockMode
Query.setLockMode
Session.lock
注意,只有在查询开始之前(也就是Hiberate 生成SQL 之前)设定加锁,才会
真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含for update
子句的Select SQL加载进来,所谓数据库加锁也就无从谈起。
乐观锁(Optimistic Locking)
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依
靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库
性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进
行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过
程中(从操作员读出数据、开始修改直至提交修改结果的全过程,甚至还包括操作
员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几
百上千个并发,这样的情况将导致怎样的后果。
乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本
(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于
数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来
实现。
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提
交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据
版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个
version字段,当前值为1;而当前帐户余额字段(balance)为$100。
1 操作员A 此时将其读出(version=1),并从其帐户余额中扣除$50
($100-$50)。
2 在操作员A操作的过程中,操作员B也读入此用户信息(version=1),并
从其帐户余额中扣除$20($100-$20)。
3 操作员A完成了修改工作,将数据版本号加一(version=2),连同帐户扣
除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本大
于数据库记录当前版本,数据被更新,数据库记录version更新为2。
4 操作员B完成了操作,也将版本号加一(version=2)试图向数据库提交数
据(balance=$80),但此时比对数据库记录版本时发现,操作员B提交的
数据版本号为2,数据库记录当前版本也为2,不满足“提交版本必须大于记
录当前版本才能执行更新“的乐观锁策略,因此,操作员B 的提交被驳回。
这样,就避免了操作员B 用基于version=1 的旧数据修改的结果覆盖操作
员A的操作结果的可能。
从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员A
和操作员B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系
统整体性能表现。
需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局
限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户
余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在
系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如
将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途
径,而不是将数据库表直接对外公开)。
Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数
据库的更新操作,利用Hibernate提供的透明化乐观锁实现,将大大提升我们的
生产力。
Hibernate中可以通过class描述符的optimistic-lock属性结合version
描述符指定。
现在,我们为之前示例中的TUser加上乐观锁机制。
1. 首先为TUser的class描述符添加optimistic-lock属性:
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
optimistic-lock="version"
>
……
</class>
</hibernate-mapping>
optimistic-lock属性有如下可选取值:
Ø none
无乐观锁
Ø version
通过版本机制实现乐观锁
Ø dirty
通过检查发生变动过的属性实现乐观锁
Ø all
通过检查所有属性实现乐观锁
其中通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也
是Hibernate中,目前唯一在数据对象脱离Session发生修改的情况下依然有效的锁机
制。因此,一般情况下,我们都选择version方式作为Hibernate乐观锁实现机制。
2. 添加一个Version属性描述符
<hibernate-mapping>
<class
name="org.hibernate.sample.TUser"
table="t_user"
dynamic-update="true"
dynamic-insert="true"
optimistic-lock="version"
>
<id
name="id"
column="id"
type="java.lang.Integer"
>
<generator class="native">
</generator>
</id>
<version
column="version"
name="version"
type="java.lang.Integer"
/>
……
</class>
</hibernate-mapping>
注意version节点必须出现在ID节点之后。
这里我们声明了一个version属性,用于存放用户的版本信息,保存在TUser表的
version字段中。
此时如果我们尝试编写一段代码,更新TUser表中记录数据,如:
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
List userList = criteria.list();
TUser user =(TUser)userList.get(0);
Transaction tx = session.beginTransaction();
user.setUserType(1); //更新UserType字段
tx.commit();
每次对TUser进行更新的时候,我们可以发现,数据库中的version都在递增。
而如果我们尝试在tx.commit 之前,启动另外一个Session,对名为Erica 的用
户进行操作,以模拟并发更新时的情形:
Session session= getSession();
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
Session session2 = getSession();
Criteria criteria2 = session2.createCriteria(TUser.class);
criteria2.add(Expression.eq("name","Erica"));
List userList = criteria.list();
List userList2 = criteria2.list();
TUser user =(TUser)userList.get(0);
TUser user2 =(TUser)userList2.get(0);
Transaction tx = session.beginTransaction();
Transaction tx2 = session2.beginTransaction();
user2.setUserType(99);
tx2.commit();
user.setUserType(1);
tx.commit();
执行以上代码,代码将在tx.commit()处抛出StaleObjectStateException异
常,并指出版本检查失败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我
们就可以在乐观锁校验失败时进行相应处理。
Hibernate分页
数据分页显示,在系统实现中往往带来了较大的工作量,对于基于JDBC的程序而言,
不同数据库提供的分页(部分读取)模式往往各不相同,也带来了数据库间可移植性上的
问题。
Hibernate中,通过对不同数据库的统一接口设计,实现了透明化、通用化的分页实
现机制。
我们可以通过Criteria.setFirstResult和Criteria.setFetchSize方法设
定分页范围,如:
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("age","20"));
//从检索结果中获取第100条记录开始的20条记录
criteria.setFirstResult(100);
criteria.setFetchSize(20);
同样,Query接口也提供了与其一致的方法。
Hibernate中,抽象类net.sf.hibernate.dialect指定了所有底层数据库的对
外统一接口。通过针对不同数据库提供相应的dialect实现,数据库之间的差异性得以消
除,从而为上层机制提供了透明的、数据库无关的存储层基础。
对于分页机制而言, dialect中定义了一个方法如下:
Public String getLimitString(
String querySelect,
boolean hasOffset
)
此方法用于在现有Select 语句基础上,根据各数据库自身特性,构造对应的记录返
回限定子句。如MySQL中对应的记录限定子句为Limit,而Oracle中,可通过rownum
子句实现。
我们来看MySQLDialect中的getLimitString实现:
public String getLimitString(String sql, boolean hasOffset) {
return new StringBuffer( sql.length()+20 )
.append(sql)
.append( hasOffset ? " limit ?, ?" : " limit ?")
.toString();
}
从上面可以看到,MySQLDialect.getLimitString方法的实现实际上是在给定的
Select语句后追加MySQL所提供的专有SQL子句limit来实现。
下面是Oracle9Dialect 中的getLimitString 实现,其中通过Oracle 特有的
rownum子句实现了数据的部分读取。
public String getLimitString(String sql, boolean hasOffset)
{
StringBuffer pagingSelect =
new StringBuffer( sql.length()+100 );
if (hasOffset) {
pagingSelect.append(
"select * from ( select row_.*, rownum rownum_ from ( "
);
}else {
pagingSelect.append("select * from ( ");
}
pagingSelect.append(sql);
if (hasOffset) {
pagingSelect.append(
" ) row_ where rownum <= ?) where rownum_ > ?"
);
}else {
pagingSelect.append(" ) where rownum <= ?");
}
return pagingSelect.toString();
}
大多数主流数据库都提供了数据部分读取机制,而对于某些没有提供相应机制的数据
库而言,Hibernate也通过其他途径实现了分页,如通过Scrollable ResultSet,如
果JDBC 不支持Scrollable ResultSet,Hibernate 也会自动通过ResultSet 的
next 方法进行记录定位。这样,Hibernate 通过底层对分页机制的良好封装,使得开发
人员无需关心数据分页的细节实现,将数据逻辑和存储逻辑分离开来,在提高生产效率的
同时,也大大加强了系统在不同数据库平台之间的可移植性。
Cache管理
Cache往往是提高系统性能的最重要的手段。在笔者记忆中,DOS时代SmartDrv2所
带来的磁盘读写性能提升还历历在目(记得95年时安装Windowns 3.0,在没有SmartDrv
常驻内存的情况下,大概需要15分钟左右,而加载了SmartDrv,只需要2分钟即可完成
整个安装过程)。
Cache对于大量倚赖数据读取操作的系统而言(典型的,如114查号系统)尤为重要,
在大并发量的情况下,如果每次程序都需要向数据库直接做查询操作,所带来的性能开销
显而易见,频繁的网络传输、数据库磁盘的读写操作(大多数数据库本身也有Cache,但
即使如此,访问数据库本身的开销也极为可观),这些都大大降低了系统的整体性能。
此时,如果能把数据在本地内存中保留一个镜像,下次访问时只需从内存中直接获取,
那么显然可以带来显著的性能提升(可能是几倍,甚至几十倍的整体读取性能提升).
引入Cache机制的难点是如何保证内存中数据的有效性,否则脏数据的出现将给系统
带来难以预知的严重后果。
Hibernate 中实现了良好的Cache 机制,我们可以借助Hibernate 内部的Cache
迅速提高系统数据读取性能。
需要注意的是:Hibernate做为一个应用级的数据访问层封装,只能在其作用范围内
保持Cache中数据的的有效性,也就是说,在我们的系统与第三方系统共享数据库的情况
下,Hibernate的Cache机制可能失效。
Hibernate 在本地JVM 中维护了一个缓冲池,并将从数据库获得的数据保存到池中
以供下次重复使用(如果在Hibernate中数据发生了变动,Hibernate同样也会更新池
中的数据版本)。
此时,如果有第三方系统对数据库进行了更改,那么,Hibernate并不知道数据库中
的数据已经发生了变化,也就是说,池中的数据还是修改之前的版本,下次读取时,
Hibernate会将此数据返回给上层代码,从而导致潜在的问题。
外部系统的定义,并非限于本系统之外的第三方系统,即使在本系统中,如果出现了
绕过Hibernate数据存储机制的其他数据存取手段,那么Cache的有效性也必须细加考
量。如,在同一套系统中,基于Hibernate和基于JDBC的两种数据访问方式并存,那么
通过JDBC更新数据库的时候,Hibernate同样无法获知数据更新的情况,从而导致脏数
据的出现。
基于Java 的Cache 实现,最简单的莫过于HashTable,hibernate 提供了基于
Hashtable 的Cache 实现机制,不过,由于其性能和功能上的局限,仅供开发调试中使
用。同时,Hibernate 还提供了面向第三方Cache 实现的接口,如JCS、EHCache、
OSCache、JBoss Cache、SwarmCache等。
Hibernate中的Cache大致分为两层,第一层Cache在Session实现,属于事务
级数据缓冲,一旦事务结束,这个Cache 也就失效。此层Cache 为内置实现,无需我们
2 DOS下的磁盘读写缓冲程序
进行干涉。
第二层Cache,是Hibernate 中对其实例范围内的数据进行缓存的管理容器。也是
这里我们讨论的主题。
Hibernate早期版本中采用了JCS(Java Caching System -Apache Turbine
项目中的一个子项目)作为默认的第二层Cache实现。由于JCS的发展停顿,以及其内在
的一些问题(在某些情况下,可能导致内存泄漏以及死锁),新版本的Hibernate已经将
JCS去除,并用EHCache作为其默认的第二级Cache实现。
相对JCS,EHCache更加稳定,并具备更好的缓存调度性能,缺陷是目前还无法做到
分布式缓存,如果我们的系统需要在多台设备上部署,并共享同一个数据库,必须使用支
持分布式缓存的Cache实现(如JCS、JBossCache)以避免出现不同系统实例之间缓存
不一致而导致脏数据的情况。
Hibernate对Cache进行了良好封装,透明化的Cache机制使得我们在上层结构的
实现中无需面对繁琐的Cache维护细节。
目前Hibernate支持的Cache实现有:
名称 类 集群支持 查询缓冲
HashTable net.sf.hibernate.cache.HashtableCacheP
rovider
N Y
EHCache net.sf.ehcache.hibernate.Provider N Y
OSCache net.sf.hibernate.cache.OSCacheProvider N Y
SwarmCache net.sf.hibernate.cache.SwarmCachePro
vider
Y
JBossCache net.sf.hibernate.cache.TreeCacheProvid
er
Y
其中SwarmCache和JBossCache均提供了分布式缓存实现(Cache集群)。
其中SwarmCache 提供的是invalidation 方式的分布式缓存,即当集群中的某个
节点更新了缓存中的数据,即通知集群中的其他节点将此数据废除,之后各个节点需要用
到这个数据的时候,会重新从数据库中读入并填充到缓存中。
而JBossCache提供的是Reapplication式的缓冲,即如果集群中某个节点的数据
发生改变,此节点会将发生改变的数据的最新版本复制到集群中的每个节点中以保持所有
节点状态一致。
使用第二层Cache,需要在hibernate.cfg.xml配置以下参数(以EHCache为例):
<hibernate-configuration>
<session-factory>
……
<property name="hibernate.cache.provider_class">
net.sf.ehcache.hibernate.Provider
</property>
……
</session-factory>
</hibernate-configuration>
另外还需要针对Cache实现本身进行配置,如EHCache的配置文件:
<ehcache>
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000" //Cache中最大允许保存的数据数量
eternal="false" //Cache中数据是否为常量
timeToIdleSeconds="120" //缓存数据钝化时间
timeToLiveSeconds="120" //缓存数据的生存时间
overflowToDisk="true" //内存不足时,是否启用磁盘缓存
/>
</ehcache>
其中“//”开始的注释是笔者追加,实际配置文件中不应出现。
之后,需要在我们的映射文件中指定各个映射实体的Cache策略:
<class name=" org.hibernate.sample.TUser" .... >
<cache usage="read-write"/>
....
<set name="addresses" .... >
<cache usage="read-only"/>
....
</set>
</class>
缓冲描述符cache可用于描述映射类和集合属性。
上例中,Class 节点下的cache 描述符指定了针对类TUser 的缓存策略为
“read-write”,即缓冲中的TUser实例为可读可写,而集合属性addresses 的缓存
策略为只读。
cache usage可选值有以下几种:
1. read-only
只读。
2. read-write
可读可写。
3. nonstrict-read-write
如果程序对并发数据修改要求不是非常严格,只是偶尔需要更新数据,可以采用
本选项,以减少无谓的检查,获得较好的性能。
4. transactional
事务性cache。在事务性Cache 中,Cache 的相关操作也被添加到事务之中,
如果由于某种原因导致事务失败,我们可以连同缓冲池中的数据一同回滚到事务
开始之前的状态。目前Hibernate 内置的Cache 中,只有JBossCache 支持
事务性的Cache实现。
不同的Cache实现,支持的usage也各不相同:
名称 read-only read-write nonstrict-read-write transactional
HashTable Y Y Y
EHCache Y Y Y
OSCache Y Y Y
SwarmCache Y Y
JBossCache Y Y
配置好Cache之后,Hibernate在运行期会自动应用Cache机制,也就是说,我们
对PO的更新,会自动同步到Cache中去,而数据的读取,也会自动化的优先从Cache中
获取,对于上层逻辑代码而言,有没有应用Cache机制,并没有什么影响。
需要注意的是Hibernate 的数据库查询机制。我们从查询结果中取出数据的时候,
用的最多的是两个方法:
Query.list();
Query.iterate();
对于list方法而言,实际上Hibernate是通过一条Select SQL获取所有的记录。
并将其读出,填入到POJO中返回。
而iterate 方法,则是首先通过一条Select SQL 获取所有符合查询条件的记录的
id,再对这个id 集合进行循环操作,通过单独的Select SQL 取出每个id 所对应的记
录,之后填入POJO中返回。
也就是说,对于list 操作,需要一条SQL 完成。而对于iterate 操作,需要n+1
条SQL。
看上去iterate方法似乎有些多余,但在不同的情况下确依然有其独特的功效,如对
海量数据的查询,如果用list方法将结果集一次取出,内存的开销可能无法承受。
另一方面,对于我们现在的Cache机制而言,list方法将不会从Cache中读取数据,
它总是一次性从数据库中直接读出所有符合条件的记录。而iterate 方法因为每次根据
id获取数据,这样的实现机制也就为从Cache读取数据提供了可能,hibernate首先会
根据这个id 在本地Cache 内寻找对应的数据,如果没找到,再去数据库中检索。如果系
统设计中对Cache比较倚重,则请注意编码中这两种不同方法的应用组合,有针对性的改
善代码,最大程度提升系统的整体性能表现。
通观以上内容,Hibernate通过对Cache的封装,对上层逻辑层而言,实现了Cache
的透明化实现,程序员编码时无需关心数据在Cache中的状态和调度,从而最大化协调了
性能和开发效率之间的平衡。
Session管理
无疑,Session是Hibernate运作的灵魂,作为贯穿Hibernate应用的关键,Session
中包含了数据库操作相关的状态信息。如对JDBC Connection 的维护,数据实体的状态维持
等。
对Session 进行有效管理的意义,类似JDBC 程序设计中对于JDBC Connection 的调
度管理。有效的Session管理机制,是Hibernate应用设计的关键。
大多数情况下,Session 管理的目标聚焦于通过合理的设计,避免Session 的频繁创建
和销毁,从而避免大量的内存开销和频繁的JVM垃圾回收,保证系统高效平滑运行。
在各种Session 管理方案中, ThreadLocal 模式得到了大量使用。ThreadLocal 是
Java中一种较为特殊的线程绑定机制。通过ThreadLocal存取的数据,总是与当前线程相关,
也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出
现的并发访问问题提供了一种隔离机制。
首先,我们需要知道,SessionFactory负责创建Session,SessionFactory是线程
安全的,多个并发线程可以同时访问一个SessionFactory 并从中获取Session 实例。而
Session并非线程安全,也就是说,如果多个线程同时使用一个Session实例进行数据存取,
则将会导致Session 数据存取逻辑混乱。下面是一个典型的Servlet,我们试图通过一个类
变量session实现Session的重用,以避免每次操作都要重新创建:
public class TestServlet extends HttpServlet {
private Session session;
public void doGet( HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
session = getSession();
doSomething();
session.flush();
}
public void doSomething(){
......//基于session的存取操作
}
}
代码看上去正确无误,甚至在我们单机测试的时候可能也不会发生什么问题,但这样的代
码一旦编译部署到实际运行环境中,接踵而来的莫名其妙的错误很可能会使得我们摸不找头脑。
问题出在哪里?
首先,Servlet 运行是多线程的,而应用服务器并不会为每个线程都创建一个Servlet
实例,也就是说,TestServlet在应用服务器中只有一个实例(在Tomcat中是这样,其他的
应用服务器可能有不同的实现),而这个实例会被许多个线程并发调用,doGet 方法也将被不
同的线程反复调用,可想而知,每次调用doGet 方法,这个唯一的TestServlet 实例的
session 变量都会被重置,线程A 的运行过程中,其他的线程如果也被执行,那么session
的引用将发生改变,之后线程A 再调用session,可能此时的session 与其之前所用的
session就不再一致,显然,错误也就不期而至。
ThreadLocal的出现,使得这个问题迎刃而解。
我们对上面的例子进行一些小小的修改:
public class TestServlet extends HttpServlet {
private ThreadLocal localSession = new ThreadLocal();
public void doGet( HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
localSession.set(getSession());
doSomething();
session.flush();
}
public void doSomething(){
Session session = (Session)localSession.get();
......//基于session的存取操作
}
}
可以看到,localSession 是一个ThreadLocal 类型的对象,在doGet 方法中,我们
通过其set 方法将获取的session 实例保存,而在doSomething 方法中,通过get 方法取
出session实例。
这也就是ThreadLocal的独特之处,它会为每个线程维护一个私有的变量空间。实际上,
其实现原理是在JVM 中维护一个Map,这个Map的key 就是当前的线程对象,而value则是
线程通过ThreadLocal.set方法保存的对象实例。当线程调用ThreadLocal.get方法时,
ThreadLocal会根据当前线程对象的引用,取出Map中对应的对象返回。
这样,ThreadLocal通过以各个线程对象的引用作为区分,从而将不同线程的变量隔离开
来。
回到上面的例子,通过应用ThreadLocal 机制,线程A 的session 实例只能为线程A
所用,同样,其他线程的session实例也各自从属于自己的线程。这样,我们就实现了线程安
全的Session共享机制。
Hibernate官方开发手册的示例中,提供了一个通过ThreadLocal维护Session的好
榜样:
public class HibernateUtil {
private static final SessionFactory sessionFactory;
static {
try {
// Create the SessionFactory
sessionFactory = new
Configuration().configure().buildSessionFactory();
} catch (HibernateException ex) {
throw new RuntimeException(
"Configuration problem: " + ex.getMessage(),
ex
);
}
}
public static final ThreadLocal session = new ThreadLocal();
public static Session currentSession() throws HibernateException
{
Session s = (Session) session.get();
// Open a new Session, if this Thread has none yet
if (s == null) {
s = sessionFactory.openSession();
session.set(s);
}
return s;
}
public static void closeSession() throws HibernateException {
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}
在代码中,只要借助上面这个工具类获取Session 实例,我们就可以实现线程范围内的
Session 共享,从而避免了在线程中频繁的创建和销毁Session 实例。不过注意在线程结束
时关闭Session。
同时值得一提的是,新版本的Hibernate在处理Session的时候已经内置了延迟加载机
制,只有在真正发生数据库操作的时候,才会从数据库连接池获取数据库连接,我们不必过于担
心Session的共享会导致整个线程生命周期内数据库连接被持续占用。
上面的HibernateUtil类可以应用在任何类型的Java程序中。特别的,对于Web程序
而言,我们可以借助Servlet2.3规范中新引入的Filter机制,轻松实现线程生命周期内的
Session管理(关于Filter的具体描述,请参考Servlet2.3规范)。
Filter的生命周期贯穿了其所覆盖的Servlet(JSP也可以看作是一种特殊的Servlet)
及其底层对象。Filter在Servlet被调用之前执行,在Servlet调用结束之后结束。因此,
在Filter 中管理Session 对于Web 程序而言就显得水到渠成。下面是一个通过Filter 进
行Session管理的典型案例:
public class PersistenceFilter implements Filter
{
protected static ThreadLocal hibernateHolder = new ThreadLocal();
public void doFilter(ServletRequest request, ServletResponse
response, FilterChain chain)
throws IOException, ServletException
{
hibernateHolder.set(getSession());
try
{
……
chain.doFilter(request, response);
……
}
finally
{
Session sess = (Session)hibernateHolder.get();
if (sess != null)
{
hibernateHolder.set(null);
try
{
sess.close();
}
catch (HibernateException ex) {
throw new ServletException(ex);
}
}
}
}
……
}
通过在doFilter中获取和关闭Session,并在周期内运行的所有对象(Filter链中其
余的Filter,及其覆盖的Servlet 和其他对象)对此Session 实例进行重用,保证了一个
Http Request处理过程中只占用一个Session,提高了整体性能表现。
在实际设计中,Session的重用做到线程级别一般已经足够,企图通过HttpSession实
现用户级的Session重用反而可能导致其他的问题。凡事不能过火,Session重用也一样。J
编后赘言
Hibernate是一个优秀的ORM实现,不过请注意,它只是一个ORM
实现而已,也不能保证是最优秀的。
笔者使用过的ORM 实现中,Apache OJB、Oracle TopLink、IBatis
和Jaxor都给笔者留下了深刻映像,是否选择Hibernate作为持久层实现,
需要结合实际情况考虑(在很多情况下,比如对遗留系统的改造项目中、
ibatis可能更加合适)。
合理的设计,冷静的取舍是考量一个系统架构师功底的最实际的标
准。常在网上看到对Hibernate 以及其他一些项目或者框架标准的狂热
鼓吹,不禁想起自己三年前逢项目必定为EJB摇旗呐喊的样子。
设计需要用时间来沉淀,建筑、服装都是如此,软件产品也一样……