目录
-
前言
- 1. 在Tomcat中快速上手
- 2. Hibernate入门
- 3. 体系结构(Architecture)
- 4. 配置
- 5. 持久化类(Persistent Classes)
- 6. 对象/关系数据库映射基础(Basic O/R Mapping)
-
-
6.1. 映射定义(Mapping declaration)
-
-
6.1.1. Doctype
6.1.2. hibernate-mapping
6.1.3. class
6.1.4. id
- 6.1.5. composite-id 6.1.6. 鉴别器(discriminator) 6.1.7. 版本(version)(可选) 6.1.8. timestamp (optional) 6.1.9. property 6.1.10. 多对一(many-to-one) 6.1.11. 一对一 6.1.12. 组件(component), 动态组件(dynamic-component) 6.1.13. properties 6.1.14. 子类(subclass) 6.1.15. 连接的子类(joined-subclass) 6.1.16. 联合子类(union-subclass) 6.1.17. 连接(join) 6.1.18. 键(key) 6.1.19. 字段和规则元素(column and formula elements) 6.1.20. 引用(import) 6.1.21. any
6.2. Hibernate 的类型
- 6.3. SQL中引号包围的标识符 6.4. 其他元数据(Metadata)
7. 集合类(Collections)映射
-
- 8. 关联关系映射
- 9. 组件(Component)映射
- 10. 继承映射(Inheritance Mappings)
-
-
10.1. 三种策略
-
-
10.1.1. 每个类分层结构一张表(Table per class hierarchy)
10.1.2. 每个子类一张表(Table per subclass)
10.1.3. 每个子类一张表(Table per subclass),使用辨别标志(Discriminator)
10.1.4. 混合使用“每个类分层结构一张表”和“每个子类一张表”
10.1.5. 每个具体类一张表(Table per concrete class)
10.1.6. Table per concrete class, using implicit polymorphism
10.1.7. 隐式多态和其他继承映射混合使用
10.2. 限制
11. 与对象共事
-
- 12. 事务和并发
- 13. 拦截器与事件(Interceptors and events)
- 14. 批量处理(Batch processing)
- 15. HQL: Hibernate查询语言
- 16. 条件查询(Criteria Queries)
- 17. Native SQL查询
- 18. 过滤数据
- 19. XML映射
- 20. 提升性能
- 21. 工具箱指南
- 22. 示例:父子关系(Parent Child Relationships)
- 23. 示例:Weblog 应用程序
- 24. 示例:复杂映射实例
- 25. 最佳实践(Best Practices)
WARNING! This is a translated version of the English Hibernate reference documentation. The translated version might not be up to date! However, the differences should only be very minor. Consult the English reference documentation if you are missing information or encounter a translation error. If you like to contribute to a particular translation, contact us on the Hibernate developer mailing list.
Translator(s): RedSaga Translate Team 满江红翻译团队 <caoxg@yahoo.com>
在今日的企业环境中,把面向对象的软件和关系数据库一起使用可能是相当麻烦、浪费时间的。Hibernate是一个面向Java环境的对象/关系数据库映射工具。对象/关系数据库映射(object/relational mapping (ORM))这个术语表示一种技术,用来把对象模型表示的对象映射到基于SQL的关系模型数据结构中去。
Hibernate不仅仅管理Java类到数据库表的映射(包括Java数据类型到SQL数据类型的映射),还提供数据查询和获取数据的方法,可以大幅度减少开发时人工使用SQL和JDBC处理数据的时间。
Hibernate的目标是对于开发者通常的数据持久化相关的编程任务,解放其中的95%。对于以数据为中心的程序来说,它们往往只在数据库中使用存储过程来实现商业逻辑,Hibernate可能不是最好的解决方案;对于那些在基于Java的中间层应用中,它们实现面向对象的业务模型和商业逻辑的应用,Hibernate是最有用的。不管怎样,Hibernate一定可以帮助你消除或者包装那些针对特定厂商的SQL代码,并且帮你把结果集从表格式的表示形式转换到一系列的对象去。
如果你对Hibernate和对象/关系数据库映射还是个新手,或者甚至对Java也不熟悉,请按照下面的步骤来学习。
阅读这个30分钟就可以结束的第 1 章 在Tomcat中快速上手,它使用Tomcat。
阅读第 2 章 Hibernate入门 ,这是一篇较长的指南,包含详细的逐步指导。
阅读第 3 章 体系结构(Architecture)来理解Hibernate可以使用的环境。
查看Hibernate发行包中的eg/目录,里面有一个简单的独立运行的程序。把你的JDBC驱动拷贝到lib/目录下,修改一下src/hibernate.properties,指定其中你的数据库的信息。进入命令行,切换到你的发行包的目录,输入ant eg(使用了Ant),或者在Windows操作系统中使用build eg。
把这份参考文档作为你学习的主要信息来源。
在Hibernate 的网站上可以找到经常提问的问题与解答(FAQ)。
在Hibernate网站上还有第三方的演示、示例和教程的链接。
Hibernate网站的“社区(Community Area)”是讨论关于设计模式以及很多整合方案(Tomcat, JBoss AS, Struts, EJB,等等)的好地方。
如果你有问题,请使用Hibernate网站上链接的用户论坛。我们也提供一个JIRA问题追踪系统,来搜集bug报告和新功能请求。如果你对开发Hibernate有兴趣,请加入开发者的邮件列表。(Hibernate网站上的用户论坛有一个中文版面,JavaEye也有Hibernate中文版面,您可以在那里交流问题与经验。)
商业开发、产品支持和Hibernate培训可以通过JBoss Inc.获得。(请查阅:http://www.hibernate.org/SupportTraining/)。 Hibernate是一个专业的开放源代码项目(Professional Open Source project),也是JBoss Enterprise Middleware System(JEMS),JBoss企业级中间件系统的一个核心组件。
本文档的翻译是在网络上协作进行的,也会不断根据Hibernate的升级进行更新。提供此文档的目的是为了减缓学习Hibernate的坡度,而非代替原文档。我们建议所有有能力的读者都直接阅读英文原文。若您对翻译有异议,或发现翻译错误,敬请不吝赐教,报告到如下email地址:cao at redsaga.com
Hibernate版本3的翻译由满江红翻译团队(RedSaga Translate Team)集体进行,这也是一次大规模网络翻译的试验。在不到20天的时间内,我们完成了两百多页文档的翻译,这一成果是通过十几位网友集体努力完成的。通过这次翻译,我们也有了一套完整的流程,从初译、技术审核一直到文字审核、发布。我们的翻译团队还会继续完善我们的翻译流程,并翻译其他优秀的Java开源资料,敬请期待。
表 1. Hibernate v3翻译团队
序号 | 标题 | 中文标题 | 翻译 | 审校 |
---|---|---|---|---|
#1 | Quickstart with Tomcat | 在Tomcat中快速上手 | 曹晓钢 | zoujm |
#2 | Architecture | 体系结构 | Hilton(BJUG) | 厌倦发呆 |
#3 | Configuration | 配置 | Goncha | mochow |
#4 | Persistent Classes | 持久化类 | 曹晓钢 | mochow |
#5 | Basic O/R Mapping | 对象/关系数据库映射基础(上) | moxie | Kingfish |
对象/关系数据库映射基础(下) | inter_dudu | vincent | ||
#6 | Collection Mapping | 集合类映射 | 曹晓钢 | robbin |
#7 | Association Mappings | 关联关系映射 | Robbin | devils.advocate |
#8 | Component Mapping | 组件映射 | 曹晓钢 | Robbin |
#9 | Inheritance Mappings | 继承映射 | morning(BJUG) | mochow |
#10 | Working with objects | 与对象共事 | 程广楠 | 厌倦发呆 |
#11 | Transactions And Concurrency | 事务和并发 | Robbin | mochow |
#12 | Interceptors and events | 继承映射 | 七彩狼(BJUG) | 厌倦发呆 |
#13 | Batch processing | 批量处理 | Kingfish(BJUG) | 厌倦发呆 |
#14 | HQL: The Hibernate Query Language | HQL: Hibernate查询语言 | 郑浩(BJUG) | zhengshuai |
#15 | Criteria Queries | 条件查询 | nemo(BJUG) | zhengshuai |
#16 | Native SQL | Native SQL查询 | 似水流年 | zoujm |
#17 | Filters | 过滤数据 | 冰云(BJUG) | Goncha |
#18 | XML Mapping | XML映射 | edward(BJUG) | Goncha |
#19 | Improving performance | 性能提升 | Wangjinfeng | Robbin |
#20 | Toolset Guide | 工具箱指南 | 曹晓钢 | Robbin |
#21 | Example: Parent/Child | 示例:父子关系 | 曹晓钢 | devils.advocate |
#22 | Example: Weblog Application | 示例:Weblog 应用程序 | 曹晓钢 | devils.advocate |
#23 | Example: Various Mappings | 示例:多种映射 | shidu(BJUG) | 冰云 |
#24 | Best Practices | 最佳实践 | 曹晓钢 | 冰云 |
关于我们
-
满江红.开源, http://www.redsaga.com
-
从成立之初就致力于Java开放源代码在中国的传播与发展,与国内多个Java团体及出版社有深入交流。坚持少说多做的原则,目前有两个团队,“OpenDoc团队”与“翻译团队”,本翻译文档即为翻译团队作品。OpenDoc团队已经推出包括Hibernate、iBatis、Spring、WebWork的多份开放文档,并于2005年5月在Hibernate开放文档基础上扩充成书,出版了原创书籍:《深入浅出Hibernate》,本书400余页,适合各个层次的Hibernate用户。(http://www.redsaga.com/hibernate_book.html)敬请支持。
北京Java用户组, http://www.bjug.org
-
Beiing Java User Group,民间技术交流组织,成立于2004年6月。以交流与共享为宗旨,每两周举行一次技术聚会活动。BJUG的目标是,通过小部分人的努力,形成一个技术社群,创建良好的交流氛围,并将新的技术和思想推广到整个IT界,让我们共同进步。
Java视线, http://www.javaeye.com
-
Java视线在是Hibernate中文论坛(http://www.hibernate.org.cn,Hibernate中文论坛是中国最早的Hibernate专业用户论坛,为Hibernate在中国的推广做出了巨大的贡献)基础上发展起来的Java深度技术网站,目标是成为一个高品质的,有思想深度的、原创精神的Java技术交流网站,为软件从业人员提供一个自由的交流技术,交流思想和交流信息的平台。
这份教程描述如何在Apache Tomcat servlet容器中为web应用程序配置Hibernate 3.0(我们使用Tomcat 4.1版本,与5.0版本差别很小)。Hibernate在大多数主流J2EE应用服务器 的运行环境中都可以工作良好,甚至也可以在独立Java应用程序中使用。在本教程中使用的示例数据库系统是PostgreSQL 7.4,只需要修改Hibernate SQL语言配置与连接属性,就可以 很容易的支持其他数据库了。
第一步,我们必须拷贝所有需要的库文件到Tomcat安装目录中。在这篇教程中,我们使用一个独立的web Context配置(webapps/quickstart)。我们确认全局库文件(TOMCAT/common/lib)和本web应用程序上下文的路径(对于jar来说是webapps/quickstart/WEB-INF/lib,对于class文件来说是webapps/quickstart/WEB-INF/classes)能够被类装载器检索到。我们把这两个类装载器级别分别称做全局类路径(global classpath)和上下文类路径(context classpath)。
现在,把这些库文件copy到两个类路径去:
把数据库需要的JDBC驱动文件拷贝到全局类路径,这是tomcat捆绑的DBCP连接池所需要的。Hibernate使用JDBC连接数据库方式执行SQL语句,所以你要么提供外部连接池中的连接给Hibernate,或者配置Hibernate自带的连接池(C3PO,Proxool)。对于本教程来说,把pg74jdbc3.jar库文件(支持PostgreSQL 7.4和JDK 1.4)到全局类装载路径下即可。如果你希望使用其他的数据库,拷贝其相应的JDBC 驱动文件)。
永远不要拷贝任何其他东西到Tomcat的全局类路径下,否则你可能在使用其他一些工具上遇到麻烦,比如log4j, commons-logging等等。 一定要让每个web应用程序使用自己的上下文类路径,就是说把你自己需要的类库拷贝到WEB-INF/lib下去,把配置文件configuration/property等配置文件拷贝到WEB-INF/classes下面去。这两个目录都是当前程序缺省的上下文类路径。
Hibernate本身打包成一个JAR类库。将hibernate3.jar文件拷贝到程序的上下文类路径下,和你应用程序的其他库文件放一起。在运行时,Hibernate还需要一些第三方类库,它们在Hibernate发行包的lib/目录下。参见表 1.1 “ Hibernate 第三方类库 ”。把所需要的第三方库文件也拷贝到上下文类路径下。
表 1.1. Hibernate 第三方类库
类库 | 描述 |
---|---|
antlr (必需) | Hibernate使用ANTLR来产生查询分析器,这个类库在运行环境下时也是必需的。 |
dom4j (必需) | Hibernate使用dom4j解析XML配置文件和XML映射元文件。 |
CGLIB ,asm(必需) | Hibernate在运行时使用这个代码生成库增强类(与Java反射机制联合使用)。 |
Commons Collections, Commons Logging (必需) | Hibernat使用Apache Jakarta Commons项目提供的多个工具类库。 |
EHCache (必需) | Hibernate可以使用不同cache缓存工具作为二级缓存。EHCache是缺省的cache缓存工具。 |
Log4j (可选) | Hibernate使用Commons Logging API,它也可以依次使用Log4j作为底层实施log的机制。如果上下文类目录中存在Log4j库,则Commons Logging使用Log4j和并它在上下文类路径中寻找的log4j.properties文件。你可以使用在Hibernate发行包中包含中的那个示例Log4j的配置文件。这样,把log4j.jar和它的配置文件(位于src/目录中)拷贝到你的上下文类路径下,就可以在后台看到底程序如何运行的。 |
其他文件是不是必需的? | 请察看Hibernate发行包中的 lib/README.txt文件,这是一个Hibernate发行包中附带的第三方类库的列表,他们总是保持最新的。你可以在那里找到所有必需或者可选的类库(注意:其中的"buildtime required"指的是编译Hibernate时所需要而非编译你自己的程序所必需的类库)。 |
接下来我们来配置在Tomcat和Hibernate中共用的数据库连接池。也就是说Tomcat会提供经过池处理的JDBC连接(用它内置的DBCP连接池),Hibernate通过JNDI方式来请求获得JDBC连接。作为替代方案,你也可以让Hibernate自行管理连接池。Tomcat把连接池绑定到JNDI,我们要在Tomcat的主配置文件(TOMCAT/conf/server.xml)中加一个资源声明:
<Context path="/quickstart" docBase="quickstart"> <Resource name="jdbc/quickstart" scope="Shareable" type="javax.sql.DataSource"/> <ResourceParams name="jdbc/quickstart"> <parameter> <name>factory</name> <value>org.apache.commons.dbcp.BasicDataSourceFactory</value> </parameter> <!-- DBCP database connection settings --> <parameter> <name>url</name> <value>jdbc:postgresql://localhost/quickstart</value> </parameter> <parameter> <name>driverClassName</name><value>org.postgresql.Driver</value> </parameter> <parameter> <name>username</name> <value>quickstart</value> </parameter> <parameter> <name>password</name> <value>secret</value> </parameter> <!-- DBCP connection pooling options --> <parameter> <name>maxWait</name> <value>3000</value> </parameter> <parameter> <name>maxIdle</name> <value>100</value> </parameter> <parameter> <name>maxActive</name> <value>10</value> </parameter> </ResourceParams> </Context>
我们在这个例子中要配置的上下文叫做quickstart,它位于TOMCAT/webapp/quickstart目录下。如果要访问这个应用程序,在你的浏览器中输入http://localhost:8080/quickstart就可以了(当然,在后面加上在你的web.xml文件中配置好你的servlet)。你现在可以创建一个只含有空process()的简单servlet了。
Tomcat现在通过JNDI的方式:java:comp/env/jdbc/quickstart来提供连接。如果你在配置连接池遇到问题,请查阅Tomcat文档。如果你遇到了JDBC驱动所报的exception出错信息,请在没有Hibernate的环境下,先测试JDBC连接池本身是否配置正确。Tomcat和JDBC的配置教程可以在Web上查到。
下一步就是配置Hibernate。首先Hibernate必须知道它如何获得JDBC连接,在这里我们使用基于XML格式的Hibernate配置文件。当然使用properties文件的进行配置,但缺少一些XML语法的特性。这个XML配置文件必须放在上下文类路径(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-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="connection.datasource">java:comp/env/jdbc/quickstart</property> <property name="show_sql">false</property> <property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property> <!-- Mapping files --> <mapping resource="Cat.hbm.xml"/> </session-factory> </hibernate-configuration>
在这里我们关闭了SQL命令的log,同时告诉Hibernate使用哪种SQL数据库用语(Dialet),以及如何得到JDBC连接(通过Tomcat声明绑定的JNDI地址)。Dialet是必需配置的,因为不同的数据库都和"SQL标准"有一些出入。不用担心,Hibernate会替你处理这些差异,Hibernate支持所有主流的商业和开放源代码数据库。
SessionFactory是Hibernate的一个概念,表示对应一个数据存储源。通过创建多个XML配置文件并在你的程序中创建多个Configuration和SessionFactory对象,就可以支持多个数据库了。
在hibernate.cfg.xml中的最后一个元素声明了Cat.hbm.xml,这是一个Hibernate XML映射文件,对应于持久化类Cat。这个文件包含了把Cat POJO类映射到数据库表(或多个数据库表)的元数据。我们稍后就回来看这个文件。下一步让我们先编写这个POJO类,然后在声明它的映射元数据。
Hibernate使用简单的Java对象(Plain Old Java Objects ,就是POJOs,有时候也称作Plain Ordinary Java Objects)这种编程模型来进行持久化。一个POJO很像JavaBean,通过getter和setter方法访问其属性,对外则隐藏了内部实现的细节(假若需要的话,Hibernate也可以直接访问其属性字段)。
package org.hibernate.examples.quickstart; public class Cat { private String id; private String name; private char sex; private float weight; public Cat() { } public String getId() { return id; } private void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public char getSex() { return sex; } public void setSex(char sex) { this.sex = sex; } public float getWeight() { return weight; } public void setWeight(float weight) { this.weight = weight; } }
Hibernate对属性使用的类型不加任何限制。所有的Java JDK类型和原始类型(比如String,char和Date)都可以被映射,也包括Java 集合(Java collections framework)中的类。你可以把它们映射成为值,值集合,或者与其他实体类相关联。id是一个特殊的属性,代表了这个类的数据库标识符(主键),对于类似于Cat这样的实体类我们强烈建议使用。Hibernate也可以使用内部标识符,但这样我们会失去一些程序架构方面的灵活性。
持久化类不需要实现什么特别的接口,也不需要从一个特别的持久化根类继承下来。Hibernate也不需要使用任何编译期处理,比如字节码增强操作,它独立的使用Java反射机制和运行时类增强(通过CGLIB)。所以不依赖于Hibernate,我们就可以把POJO的类映射成为数据库表。
Cat.hbm.xml映射文件包含了对象/关系映射(O/R Mapping)所需的元数据。元数据包含持久化类的声明和属性到数据库的映射(指向字段和其他实体的外键关联)。
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="org.hibernate.examples.quickstart.Cat" table="CAT"> <!-- A 32 hex character is our surrogate key. It's automatically generated by Hibernate with the UUID pattern. --> <id name="id" type="string" unsaved-value="null" > <column name="CAT_ID" sql-type="char(32)" not-null="true"/> <generator class="uuid.hex"/> </id> <!-- A cat has to have a name, but it shouldn' be too long. --> <property name="name"> <column name="NAME" length="16" not-null="true"/> </property> <property name="sex"/> <property name="weight"/> </class> </hibernate-mapping>
每个持久化类都应该有一个标识属性(实际上,这个类只代表实体,而不是独立的值类型类,后者会被映射称为实体对象中的一个组件)。这个属性用来区分持久化对象:如果catA.getId().equals(catB.getId())结果是true的话,这两个Cat就是相同的。这个概念称为数据库标识。Hiernate附带了几种不同的标识符生成器,用于不同的场合(包括数据库本地的顺序(sequence)生成器、hi/lo高低位标识模式、和程序自己定义的标识符)。我们在这里使用UUID生成器(只在测试时建议使用,如果使用数据库自己生成的整数类型的键值更好),并指定CAT表中的CAT_ID字段(作为表的主键)存放生成的标识值。
Cat的其他属性都映射到同一个表的字段。对name属性来说,我们把它显式地声明映射到一个数据库字段。如果数据库schema是通过由映射声明使用Hibernate的SchemaExport工具自动生成的(作为SQL DDL指令)的话,这就特别有用。所有其它的属性都用Hibernate的默认值映射,大多数情况你都会这样做。数据库中的CAT表看起来是这样的:
Column | Type | Modifiers --------+-----------------------+----------- cat_id | character(32) | not null name | character varying(16) | not null sex | character(1) | weight | real | Indexes: cat_pkey primary key btree (cat_id)
你现在可以在你的数据库中手工创建这个表了,如果你需要使用hbm2ddl工具把这个步骤自动化,请参阅第 21 章 工具箱指南。这个工具能够创建完整的SQL DDL,包括表定义,自定义的字段类型约束,惟一约束和索引。
我们现在可以开始Hibernate的Session了。它是一个持久化管理器,我们通过它来从数据库中存取Cat。首先,我们要从SessionFactory中获取一个Session(Hibernate的工作单元)。
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
通过对configure()的调用来装载hibernate.cfg.xml配置文件,并初始化成一个Configuration实例。 在创建 SessionFactory之前(它是不可变的),你可以访问Configuration来设置其他属性(甚至修改映射的元数据)。我们应该在哪儿创建SessionFactory,在我们的程序中又如何访问它呢? SessionFactory通常只是被初始化一次,比如说通过一个load-on-startup servlet的来初始化。这意味着你不应该在serlvet中把它作为一个实例变量来持有,而应该放在其他地方。进一步的说,我们需要使用单例(Singleton)模式,我们才能更容易的在程序中访问SessionFactory。下面的方法就同时解决了两个问题:对SessionFactory的初始配置与便捷使用。
我们实现一个HibernateUtil辅助类:
import org.hibernate.*; import org.hibernate.cfg.*; public class HibernateUtil { private static Log log = LogFactory.getLog(HibernateUtil.class); private static final SessionFactory sessionFactory; static { try { // Create the SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory(); } catch (Throwable ex) { // Make sure you log the exception, as it might be swallowed log.error("Initial SessionFactory creation failed.", ex); throw new ExceptionInInitializerError(ex); } } public static final ThreadLocal session = new ThreadLocal(); public static Session currentSession() { 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() { Session s = (Session) session.get(); if (s != null) s.close(); session.set(null); } }
这个类不但在它的静态初始器中使用了SessionFactory,还使用了一个ThreadLocal变量来保存Session做为当前工作线程。在你使用这个辅助类之前,请确保你理解了thread-local变量这个Java概念。你可以在CaveatEmptor(http://caveatemptor.hibernate.org/)上找到一个更加复杂和强大的 HibernateUtil。
SessionFactory是安全线程,可以由很多线程并发访问并获取到Sessions。单个Session不是安全线程对象,它只代表与数据库之间的一次操作。Session通过SessionFactory获得并在所有的工作完成后关闭。在你servlet的process()中可以象是这么写的(省略了异常情况处理):
Session session = HibernateUtil.currentSession(); Transaction tx= session.beginTransaction(); Cat princess = new Cat(); princess.setName("Princess"); princess.setSex('F'); princess.setWeight(7.4f); session.save(princess); tx.commit(); HibernateUtil.closeSession();
在一个Session中,每个数据库操作都是在一个事务(transaction)中进行的,这样就可以隔离开不同的操作(甚至包括只读操作)。我们使用Hibernate的Transaction API来从底层的事务策略中(本例中是JDBC事务)脱身出来。这样,我们就不需要更改任何源代码,就可以把我们的程序部署到一个由容器管理事务的环境中去(使用JTA)。
这样你就可以随心所欲的多次调用HibernateUtil.currentSession();,你每次都会得到同一个当前线程的Session。不管是在你的servlet代码中,或者在servlet filter中还是在HTTP结果返回之前,你都必须确保这个Session在你的数据库访问工作完成后关闭。这样做还有一个好处就是可以容易的使用延迟装载(lazy initialization):Session在渲染view层的时候仍然打开着的,所以你在遍历当前对象图的时候可以装载所需的对象。
Hibernate有不同的方法用来从数据库中取回对象。最灵活的方式就是使用Hibernate查询语言(HQL),这是一种容易学习的语言,是对SQL的面向对象的强大扩展。
Transaction tx= session.beginTransaction(); Query query = session.createQuery("select c from Cat as c where c.sex = :sex"); query.setCharacter("sex", 'F'); for (Iterator it = query.iterate(); it.hasNext();) { Cat cat = (Cat) it.next(); out.println("Female Cat: " + cat.getName() ); } tx.commit();
Hibernate也提供一种面向对象的按条件查询API,可以执行简洁安全类型的查询。当然,Hibernate在所有与数据库的交互中都使用PrepatedStatement和参数绑定。你也可以使用Hibernate的直接SQL查询特性,或者在特殊情况下从Session获取一个原始的JDBC连接。
本章是面向Hibernate初学者的一个介绍教程。我们将使用容易理解的方式,开发一个使用驻留内存式(in-memory)数据库的简单命令行程序。
本教程是面向Hibernate初学者,但是需要一定的Java和SQL知识。 它在Michael Goegl所写的一个教程的基础上完成的。我们使用的第三方库文件是支持JDK 1.4和5.0。如果你要使用JDK1.3,可能会需要其它的库。
首先我们将创建一个简单的控制台(console-based)Hibernate程序。我们使用内置数据库(in-memory database) (HSQL DB),所以我们不必安装任何数据库服务器。
让我们假设我们希望有一个小程序可以保存我们希望关注的事件(Event)和这些事件的信息。 (译者注:在本教程的后面部分,我们将直接使用Event而不是它的中文翻译“事件”,以免混淆。)
我们做的第一件事是建立我们的开发目录,并把所有需要用到的Java库文件放进去。 从Hibernate网站的下载页面下载Hibernate分发版本。 解压缩包并把/lib下面的所有库文件放到我们新的开发目录下面的/lib目录下面。 看起来就像这样:
. +lib antlr.jar cglib-full.jar asm.jar asm-attrs.jars commons-collections.jar commons-logging.jar ehcache.jar hibernate3.jar jta.jar dom4j.jar log4j.jar
This is the minimum set of required libraries (note that we also copied hibernate3.jar, the main archive) for Hibernate. See the README.txt file in the lib/ directory of the Hibernate distribution for more information about required and optional third-party libraries. (Actually, Log4j is not required but preferred by many developers.) 这个是Hibernate运行所需要的最小库文件集合(注意我们也拷贝了Hibernate3.jar,这个是最重要的库)。 可以在Hibernate分发版本的lib/目录下查看README.txt,以获取更多关于所需和可选的第三方库文件信息 (事实上,Log4j并不是必须的库文件但是许多开发者都喜欢用它)。
接下来我们创建一个类,用来代表那些我们希望储存在数据库里面的event.
我们的第一个持久化类是 一个简单的JavaBean class,带有一些简单的属性(property)。 让我们来看一下代码:
import java.util.Date; public class Event { private Long id; private String title; private Date date; Event() {} public Long getId() { return id; } private void setId(Long id) { this.id = id; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } }
你可以看到这个class对属性(property)的存取方法(getter and setter method) 使用标准的JavaBean命名约定,同时把内部字段(field)隐藏起来(private visibility)。 这个是个受推荐的设计方式,但并不是必须这样做。 Hibernate也可以直接访问这些字段(field),而使用访问方法(accessor method)的好处是提供了程序重构的时候健壮性(robustness)。
id 属性(property) 为一个Event实例提供标识属性(identifier property)的值- 如果我们希望使用Hibernate的所有特性,那么我们所有的持久性实体类(persistent entity class)(这里也包括一些次要依赖类) 都需要一个标识属性(identifier property)。而事实上,大多数应用程序(特别是web应用程序)都需要识别特定的对象,所以你应该 考虑使用标识属性而不是把它当作一种限制。然而,我们通常不会直接操作一个对象的标识符(identifier), 因此标识符的setter方法应该被声明为私有的(private)。这样当一个对象被保存的时候,只有Hibernate可以为它分配标识符。 你会发现Hibernate可以直接访问被声明为public,private和protected等不同级别访问控制的方法(accessor method)和字段(field)。 所以选择哪种方式来访问属性是完全取决于你,你可以使你的选择与你的程序设计相吻合。
所有的持久类(persistent classes)都要求有无参的构造器(no-argument constructor); 因为Hibernate必须要使用Java反射机制(Reflection)来实例化对象。构造器(constructor)的访问控制可以是私有的(private), 然而当生成运行时代理(runtime proxy)的时候将要求使用至少是package级别的访问控制,这样在没有字节码编入 (bytecode instrumentation)的情况下,从持久化类里获取数据会更有效率一些。
我们把这个Java源代码文件放到我们的开发目录下面一个叫做src的目录里。 这个目录现在应该看起来像这样:
. +lib <Hibernate and third-party libraries> +src Event.java
在下一步里,我们将把这个持久类(persisten class)的信息通知Hibernate
Hibernate需要知道怎样去加载(load)和存储(store)我们的持久化类的对象。这里正是Hibernate映射文件(mapping file)发挥作用的地方。 映射文件告诉Hibernate它应该访问数据库里面的哪个表(table)和应该使用表里面的哪些字段(column)。
一个映射文件的基本结构看起来像这样:
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> [...] </hibernate-mapping>
注意Hibernate的DTD是非常复杂的。 你可以在你的编辑器或者IDE里面使用它来自动提示并完成(auto-completion)那些用来映射的XML元素(element)和属性(attribute)。 你也可以用你的文本编辑器打开DTD-这是最简单的方式来浏览所有元素和参数,查看它们的缺省值以及它们的注释,以得到一个整体的概观。 同时也要注意Hibernate不会从web上面获取DTD文件,虽然XML里面的URL也许会建议它这样做,但是Hibernate会首先查看你的程序的classpath。 DTD文件被包括在hibernate3.jar,同时也在Hibernate分发版的src/路径下。
在以后的例子里面,我们将通过省略DTD的声明来缩短代码长度。但是显然,在实际的程序中,DTD声明是必须的。
在两个hibernate-mapping标签(tag)中间, 我们包含了一个 class元素(element)。所有的持久性实体类(persistent entity classes)(再次声明, 这里也包括那些依赖类,就是那些次要的实体)都需要一个这样的映射,来映射到我们的SQL database。
<hibernate-mapping> <class name="Event" table="EVENTS"> </class> </hibernate-mapping>
我们到现在为止做的一切是告诉Hibernate怎样从数据库表(table)EVENTS里持久化和 加载Event类的对象,每个实例对应数据库里面的一行。现在我们将继续讨论有关唯一标识属性(unique identifier property)的映射。 另外,我们不希望去考虑怎样产生这个标识属性,我们将配置Hibernate的标识符生成策略(identifier generation strategy)来产生代用主键。
<hibernate-mapping> <class name="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="increment"/> </id> </class> </hibernate-mapping>
id元素是标识属性(identifer property)的声明, name="id" 声明了Java属性(property)的名字 - Hibernate将使用getId()和setId()来访问它。 字段参数(column attribute)则告诉Hibernate我们使用EVENTS表的哪个字段作为主键。 嵌套的generator元素指定了标识符的生成策略 - 在这里我们使用increment,这个是非常简单的在内存中直接生成数字的方法,多数用于测试(或教程)中。 Hibernate同时也支持使用数据库生成(database generated),全局唯一性(globally unique)和应用程序指定(application assigned) (或者你自己为任何已有策略所写的扩展) 这些方式来生成标识符。
最后我们还必须在映射文件里面包括需要持久化属性的声明。缺省的情况下,类里面的属性都被视为非持久化的:
<hibernate-mapping> <class name="Event" table="EVENTS"> <id name="id" column="EVENT_ID"> <generator class="increment"/> </id> <property name="date" type="timestamp" column="EVENT_DATE"/> <property name="title"/> </class> </hibernate-mapping>
和id元素类似,property元素的name参数 告诉Hibernate使用哪个getter和setter方法。
为什么date属性的映射包括column参数,但是title却没有? 当没有设定column参数的时候,Hibernate缺省使用属性名作为字段(column)名。对于title,这样工作得很好。 然而,date在多数的数据库里,是一个保留关键字,所以我们最好把它映射成另外一个名字。
下一件有趣的事情是title属性缺少一个type参数。 我们声明并使用在映射文件里面的type,并不像我们假想的那样,是Java data type, 同时也不是SQL database type。这些类型被称作Hibernate mapping types, 它们把数据类型从Java转换到SQL data types。如果映射的参数没有设置的话,Hibernate也将尝试去确定正确的类型转换和它的映射类型。 在某些情况下这个自动检测(在Java class上使用反射机制)不会产生你所期待或者 需要的缺省值。这里有个例子是关于date属性。Hibernate无法知道这个属性应该被映射成下面这些类型中的哪一个: SQL date,timestamp,time。 我们通过声明属性映射timestamp来表示我们希望保存所有的关于日期和时间的信息。
这个映射文件(mapping file)应该被保存为Event.hbm.xml,和我们的EventJava 源文件放在同一个目录下。映射文件的名字可以是任意的,然而hbm.xml已经成为Hibernate开发者社区的习惯性约定。 现在目录应该看起来像这样:
. +lib <Hibernate and third-party libraries> +src Event.java Event.hbm.xml
我们继续进行Hibernate的主要配置。
我们现在已经有了一个持久化类和它的映射文件,是时候配置Hibernate了。在我们做这个之前,我们需要一个数据库。 HSQL DB,一个java-based内嵌式SQL数据库(in-memory SQL Database),可以从HSQL DB的网站上下载。 实际上,你仅仅需要下载/lib/目录中的hsqldb.jar。把这个文件放在开发文件夹的lib/目录里面。
在开发目录下面创建一个叫做data的目录 - 这个是HSQL DB存储它的数据文件的地方。
Hibernate是你的程序里连接数据库的那个应用层,所以它需要连接用的信息。连接(connection)是通过一个也由我们配置的JDBC连接池(connection pool)。 Hibernate的分发版里面包括了一些open source的连接池,但是我们已经决定在这个教程里面使用内嵌式连接池。 如果你希望使用一个产品级的第三方连接池软件,你必须拷贝所需的库文件去你的classpath并使用不同的连接池设置。
为了配置Hibernate,我们可以使用一个简单的hibernate.properties文件, 或者一个稍微复杂的hibernate.cfg.xml,甚至可以完全使用程序来配置Hibernate。 多数用户喜欢使用XML配置文件:
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <!-- Database connection settings --> <property name="connection.driver_class">org.hsqldb.jdbcDriver</property> <property name="connection.url">jdbc:hsqldb:data/tutorial</property> <property name="connection.username">sa</property> <property name="connection.password"></property> <!-- JDBC connection pool (use the built-in) --> <property name="connection.pool_size">1</property> <!-- SQL dialect --> <property name="dialect">org.hibernate.dialect.HSQLDialect</property> <!-- Echo all executed SQL to stdout --> <property name="show_sql">true</property> <!-- Drop and re-create the database schema on startup --> <property name="hbm2ddl.auto">create</property> <mapping resource="Event.hbm.xml"/> </session-factory> </hibernate-configuration>
注意这个XML配置使用了一个不同的DTD。我们配置Hibernate的SessionFactory- 一个关联于特定数据库全局性的工厂(factory)。如果你要使用多个数据库,通常应该在多个配置文件中使用多个<session-factory> 进行配置(在更早的启动步骤中进行)。
最开始的4个property元素包含必要的JDBC连接信息。dialectproperty 表明Hibernate应该产生针对特定数据库语法的SQL语句。hbm2ddl.auto选项将自动生成数据库表定义(schema)- 直接插入数据库中。当然这个选项也可以被关闭(通过去除这个选项)或者通过Ant任务SchemaExport来把数据库表定义导入一个文件中进行优化。 最后,为持久化类加入映射文件。
把这个文件拷贝到源代码目录下面,这样它就位于classpath的root路径上。Hibernate在启动时会自动 在它的根目录开始寻找名为hibernate.cfg.xml的配置文件。
在这个教程里面,我们将用Ant来编译程序。你必须先安装Ant-可以从Ant download page 下载它。怎样安装Ant不是这个教程的内容,请参考Ant manual。 当你安装完了Ant,我们就可以开始创建编译脚本,它的文件名是build.xml,把它直接放在开发目录下面。
完善Ant
注意Ant的分发版通常功能都是不完整的(就像Ant FAQ里面说得那样),所以你常常不得不需要自己动手来完善Ant。 例如:如果你希望在你的build文件里面使用JUnit功能。为了让JUnit任务被激活(这个教程里面我们并不需要这个任务), 你必须拷贝junit.jar到ANT_HOME/lib目录下或者删除ANT_HOME/lib/ant-junit.jar这个插件。
一个基本的build文件看起来像这样
<project name="hibernate-tutorial" default="compile"> <property name="sourcedir" value="${basedir}/src"/> <property name="targetdir" value="${basedir}/bin"/> <property name="librarydir" value="${basedir}/lib"/> <path id="libraries"> <fileset dir="${librarydir}"> <include name="*.jar"/> </fileset> </path> <target name="clean"> <delete dir="${targetdir}"/> <mkdir dir="${targetdir}"/> </target> <target name="compile" depends="clean, copy-resources"> <javac srcdir="${sourcedir}" destdir="${targetdir}" classpathref="libraries"/> </target> <target name="copy-resources"> <copy todir="${targetdir}"> <fileset dir="${sourcedir}"> <exclude name="**/*.java"/> </fileset> </copy> </target> </project>
这个将告诉Ant把所有在lib目录下以.jar结尾的文件加入classpath中用来进行编译。 它也将把所有的非Java源代码文件,例如配置和Hibernate映射文件,拷贝到目标目录下。如果你现在运行Ant, 你将得到以下输出:
C:/hibernateTutorial/>ant Buildfile: build.xml copy-resources: [copy] Copying 2 files to C:/hibernateTutorial/bin compile: [javac] Compiling 1 source file to C:/hibernateTutorial/bin BUILD SUCCESSFUL Total time: 1 second
是时候来加载和储存一些Event对象了,但是首先我们不得不完成一些基础的代码。 我们必须启动Hibernate。这个启动过程包括创建一个全局性的SessoinFactory并把它储存在一个应用程序容易访问的地方。 SessionFactory可以创建并打开新的Session。 一个Session代表一个单线程的单元操作,SessionFactory则是一个线程安全的全局对象,只需要创建一次。
我们将创建一个HibernateUtil帮助类(helper class)来负责启动Hibernate并使 操作Session变得容易。这个帮助类将使用被称为ThreadLocal Session 的模式来保证当前的单元操作和当前线程相关联。让我们来看一眼它的实现:
import org.hibernate.*; import org.hibernate.cfg.*; public class HibernateUtil { public static final SessionFactory sessionFactory; static { try { // Create the SessionFactory from hibernate.cfg.xml sessionFactory = new Configuration().configure().buildSessionFactory(); } catch (Throwable ex) { // Make sure you log the exception, as it might be swallowed System.err.println("Initial SessionFactory creation failed." + ex); throw new ExceptionInInitializerError(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(); // Store it in the ThreadLocal variable session.set(s); } return s; } public static void closeSession() throws HibernateException { Session s = (Session) session.get(); if (s != null) s.close(); session.set(null); } }
这个类不仅仅在它的静态初始化过程(仅当加载这个类的时候被JVM执行一次)中产生全局SessionFactory, 同时也有一个ThreadLocal变量来为当前线程保存Session。不论你何时 调用HibernateUtil.currentSession(),它总是返回同一个线程中的同一个Hibernate单元操作。 而一个HibernateUtil.closeSession()调用将终止当前线程相联系的那个单元操作。
在你使用这个帮助类之前,确定你明白Java关于本地线程变量(thread-local variable)的概念。一个功能更加强大的 HibernateUtil帮助类可以在CaveatEmptorhttp://caveatemptor.hibernate.org/找到 -它同时也出现在书:《Hibernate in Action》中。注意当你把Hibernate部署在一个J2EE应用服务器上的时候,这个类不是必须的: 一个Session会自动绑定到当前的JTA事物上,你可以通过JNDI来查找SessionFactory。 如果你使用JBoss AS,Hibernate可以被部署成一个受管理的系统服务(system service)并自动绑定SessionFactory到JNDI上。
把HibernateUtil.java放在开发目录的源代码路径下面,与 Event.java放在一起:
. +lib <Hibernate and third-party libraries> +src Event.java Event.hbm.xml HibernateUtil.java hibernate.cfg.xml +data build.xml
再次编译这个程序不应该有问题。最后我们需要配置一个日志系统 - Hibernate使用通用日志接口,这允许你在Log4j和 JDK 1.4 logging之间进行选择。多数开发者喜欢Log4j:从Hibernate的分发版(它在etc/目录下)拷贝 log4j.properties到你的src目录,与hibernate.cfg.xml.放在一起。 看一眼配置示例,你可以修改配置如果你希望看到更多的输出信息。缺省情况下,只有Hibernate的启动信息会显示在标准输出上。
教程的基本框架完成了 - 现在我们可以用Hibernate来做些真正的工作。
终于,我们可以使用Hibernate来加载和存储对象了。我们编写一个带有main()方法 的EventManager类:
import org.hibernate.Transaction; import org.hibernate.Session; import java.util.Date; public class EventManager { public static void main(String[] args) { EventManager mgr = new EventManager(); if (args[0].equals("store")) { mgr.createAndStoreEvent("My Event", new Date()); } HibernateUtil.sessionFactory.close(); } }
我们从命令行读入一些参数,如果第一个参数是"store",我们创建并储存一个新的Event:
private void createAndStoreEvent(String title, Date theDate) { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); Event theEvent = new Event(); theEvent.setTitle(title); theEvent.setDate(theDate); session.save(theEvent); tx.commit(); HibernateUtil.closeSession(); }
我们创建一个新的Event对象并把它传递给Hibernate。Hibernate现在负责创建SQL并把 INSERT命令传给数据库。在运行它之前,让我们花一点时间在Session和Transaction的处理代码上。
每个Session是一个独立的单元操作。你会对我们有另外一个API:Transaction而感到惊奇。 这暗示一个单元操作可以拥有比一个单独的数据库事务更长的生命周期 - 想像在web应用程序中,一个单元操作跨越多个Http request/response循环 (例如一个创建对话框)。根据“应用程序用户眼中的单元操作”来切割事务是Hibernate的基本设计思想之一。我们调用 一个长生命期的单元操作Application Transaction时,通常包装几个更生命期较短的数据库事务。 为了简化问题,在这个教程里我们使用Session和Transaction之间是1对1关系的粒度(one-to-one granularity)。
Transaction.begin()和commit()都做些什么?rollback()在哪些情况下会产生错误? Hibernate的Transaction API 实际上是可选的, 但是我们通常会为了便利性和可移植性而使用它。 如果你宁可自己处理数据库事务(例如,调用session.connection.commit()),通过直接和无管理的JDBC,这样将把代码绑定到一个特定的部署环境中去。 通过在Hibernate配置中设置Transaction工厂,你可以把你的持久化层部署在任何地方。 查看第 12 章 事务和并发了解更多关于事务处理和划分的信息。在这个例子中我们也忽略任何异常处理和事务回滚。
为了第一次运行我们的应用程序,我们必须增加一个可以调用的target到Ant的build文件中。
<target name="run" depends="compile"> <java fork="true" classname="EventManager" classpathref="libraries"> <classpath path="${targetdir}"/> <arg value="${action}"/> </java> </target>
action参数的值是在通过命令行调用这个target的时候设置的:
C:/hibernateTutorial/>ant run -Daction=store
你应该会看到,编译结束以后,Hibernate根据你的配置启动,并产生一大堆的输出日志。在日志最后你会看到下面这行:
[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)
这是Hibernate执行的INSERT命令,问号代表JDBC的待绑定参数。如果想要看到绑定参数的值或者减少日志的长度, 检查你在log4j.properties文件里的设置。
现在我们想要列出所有已经被存储的event,所以我们增加一个条件分支选项到main方法中去。
if (args[0].equals("store")) { mgr.createAndStoreEvent("My Event", new Date()); } else if (args[0].equals("list")) { List events = mgr.listEvents(); for (int i = 0; i < events.size(); i++) { Event theEvent = (Event) events.get(i); System.out.println("Event: " + theEvent.getTitle() + " Time: " + theEvent.getDate()); } }
我们也增加一个新的listEvents()方法:
private List listEvents() { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); List result = session.createQuery("from Event").list(); tx.commit(); session.close(); return result; }
我们在这里是用一个HQL(Hibernate Query Language-Hibernate查询语言)查询语句来从数据库中 加载所有存在的Event。Hibernate会生成正确的SQL,发送到数据库并使用查询到的数据来生成Event对象。 当然你也可以使用HQL来创建更加复杂的查询。
如果你现在使用命令行参数-Daction=list来运行Ant,你会看到那些至今为止我们储存的Event。 如果你是一直一步步的跟随这个教程进行的,你也许会吃惊这个并不能工作 - 结果永远为空。原因是hbm2ddl.auto 打开了一个Hibernate的配置选项:这使得Hibernate会在每次运行的时候重新创建数据库。通过从配置里删除这个选项来禁止它。 运行了几次store之后,再运行list,你会看到结果出现在列表里。 另外,自动生成数据库表并导出在单元测试中是非常有用的。
我们已经映射了一个持久化实体类到一个表上。让我们在这个基础上增加一些类之间的关联性。 首先我们往我们程序里面增加人(people)的概念,并存储他们所参与的一个Event列表。 (译者注:与Event一样,我们在后面的教程中将直接使用person来表示“人”而不是它的中文翻译)
最初的Person类是简单的:
public class Person { private Long id; private int age; private String firstname; private String lastname; Person() {} // Accessor methods for all properties, private setter for 'id' }
Create a new mapping file called Person.hbm.xml:
<hibernate-mapping> <class name="Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="increment"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/> </class> </hibernate-mapping>
Finally, add the new mapping to Hibernate's configuration:
<mapping resource="Event.hbm.xml"/> <mapping resource="Person.hbm.xml"/>
我们现在将在这两个实体类之间创建一个关联。显然,person可以参与一系列Event,而Event也有不同的参加者(person)。 设计上面我们需要考虑的问题是关联的方向(directionality),阶数(multiplicity)和集合(collection)的行为。
我们将向Person类增加一组Event。这样我们可以轻松的通过调用aPerson.getEvents() 得到一个Person所参与的Event列表,而不必执行一个显式的查询。我们使用一个Java的集合类:一个Set,因为Set 不允许包括重复的元素而且排序和我们无关。
目前为止我们设计了一个单向的,在一端有许多值与之对应的关联,通过Set来实现。 让我们为这个在Java类里编码并映射这个关联:
public class Person { private Set events = new HashSet(); public Set getEvents() { return events; } public void setEvents(Set events) { this.events = events; } }
在我们映射这个关联之前,先考虑这个关联另外一端。很显然的,我们可以保持这个关联是单向的。如果我们希望这个关联是双向的, 我们可以在Event里创建另外一个集合,例如:anEvent.getParticipants()。 这是留给你的一个设计选项,但是从这个讨论中我们可以很清楚的了解什么是关联的阶数(multiplicity):在这个关联的两端都是“多”。 我们叫这个为:多对多(many-to-many)关联。因此,我们使用Hibernate的many-to-many映射:
<class name="Person" table="PERSON"> <id name="id" column="PERSON_ID"> <generator class="increment"/> </id> <property name="age"/> <property name="firstname"/> <property name="lastname"/> <set name="events" table="PERSON_EVENT"> <key column="PERSON_ID"/> <many-to-many column="EVENT_ID" class="Event"/> </set> </class>
Hibernate支持所有种类的集合映射,<set>是最普遍被使用的。对于多对多(many-to-many)关联(或者叫n:m实体关系), 需要一个用来储存关联的表(association table)。表里面的每一行代表从一个person到一个event的一个关联。 表名是由set元素的table属性值配置的。关联里面的标识字段名,person的一端,是 由<key>元素定义,event一端的字段名是由<many-to-many>元素的 column属性定义的。你也必须告诉Hibernate集合中对象的类(也就是位于这个集合所代表的关联另外一端的类)。
这个映射的数据库表定义如下:
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | |_____________| |__________________| | PERSON | | | | | |_____________| | *EVENT_ID | <--> | *EVENT_ID | | | | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | |_____________| | FIRSTNAME | | LASTNAME | |_____________|
让我们把一些people和event放到EventManager的一个新方法中:
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); Person aPerson = (Person) session.load(Person.class, personId); Event anEvent = (Event) session.load(Event.class, eventId); aPerson.getEvents().add(anEvent); tx.commit(); HibernateUtil.closeSession(); }
在加载一个Person和一个Event之后,简单的使用普通的方法修改集合。 如你所见,没有显式的update()或者save(), Hibernate自动检测到集合已经被修改 并需要保存。这个叫做automatic dirty checking,你也可以尝试修改任何对象的name或者date的参数。 只要他们处于persistent状态,也就是被绑定在某个Hibernate Session上(例如:他们 刚刚在一个单元操作从被加载或者保存),Hibernate监视任何改变并在后台隐式执行SQL。同步内存状态和数据库的过程,通常只在 一个单元操作结束的时候发生,这个过程被叫做flushing。
你当然也可以在不同的单元操作里面加载person和event。或者在一个Session以外修改一个 不是处在持久化(persistent)状态下的对象(如果该对象以前曾经被持久化,我们称这个状态为脱管(detached))。 在程序里,看起来像下面这样:
private void addPersonToEvent(Long personId, Long eventId) { Session session = HibernateUtil.currentSession(); Transaction tx = session.beginTransaction(); Person aPerson = (Person) session.load(Person.class, personId); Event anEvent = (Event) session.load(Event.class, eventId); tx.commit(); HibernateUtil.closeSession(); aPerson.getEvents().add(anEvent); // aPerson is detached Session session2 = HibernateUtil.currentSession(); Transaction tx2 = session.beginTransaction(); session2.update(aPerson); // Reattachment of aPerson tx2.commit(); HibernateUtil.closeSession(); }
对update的调用使一个脱管对象(detached object)重新持久化,你可以说它被绑定到 一个新的单元操作上,所以任何你对它在脱管(detached)状态下所做的修改都会被保存到数据库里。
这个对我们当前的情形不是很有用,但是它是非常重要的概念,你可以把它设计进你自己的程序中。现在,加进一个新的 选项到EventManager的main方法中,并从命令行运行它来完成这个练习。如果你需要一个person和 一个event的标识符 - save()返回它。*******这最后一句看不明白
上面是一个关于两个同等地位的类间关联的例子,这是在两个实体之间。像前面所提到的那样,也存在其它的特别的类和类型,这些类和类型通常是“次要的”。 其中一些你已经看到过,好像int或者String。我们称呼这些类为值类型(value type), 它们的实例依赖(depend)在某个特定的实体上。这些类型的实例没有自己的身份(identity),也不能在实体间共享 (比如两个person不能引用同一个firstname对象,即使他们有相同的名字)。当然,value types并不仅仅在JDK中存在 (事实上,在一个Hibernate程序中,所有的JDK类都被视为值类型),你也可以写你自己的依赖类,例如Address, MonetaryAmount。
你也可以设计一个值类型的集合(collection of value types),这个在概念上与实体的集合有很大的不同,但是在Java里面看起来几乎是一样的。
我们把一个值类型对象的集合加入Person。我们希望保存email地址,所以我们使用String, 而这次的集合类型又是Set:
private Set emailAddresses = new HashSet(); public Set getEmailAddresses() { return emailAddresses; } public void setEmailAddresses(Set emailAddresses) { this.emailAddresses = emailAddresses; }
Set的映射
<set name="emailAddresses" table="PERSON_EMAIL_ADDR"> <key column="PERSON_ID"/> <element type="string" column="EMAIL_ADDR"/> </set>
比较这次和较早先的映射,差别主要在element部分这次并没有包括对其它实体类型的引用,而是使用一个元素类型是 String的集合(这里使用小写的名字是向你表明它是一个Hibernate的映射类型或者类型转换器)。 和以前一样,set的table参数决定用于集合的数据库表名。key元素 定义了在集合表中使用的外键。element元素的column参数定义实际保存String值 的字段名。
看一下修改后的数据库表定义。
_____________ __________________ | | | | _____________ | EVENTS | | PERSON_EVENT | | | ___________________ |_____________| |__________________| | PERSON | | | | | | | |_____________| | PERSON_EMAIL_ADDR | | *EVENT_ID | <--> | *EVENT_ID | | | |___________________| | EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID | <--> | *PERSON_ID | | TITLE | |__________________| | AGE | | *EMAIL_ADDR | |_____________| | FIRSTNAME | |___________________| | LASTNAME | |_____________|
你可以看到集合表(collection table)的主键实际上是个复合主键,同时使用了2个字段。这也暗示了对于同一个 person不能有重复的email地址,这正是Java里面使用Set时候所需要的语义(Set里元素不能重复)。
你现在可以试着把元素加入这个集合,就像我们在之前关联person和event的那样。Java里面的代码是相同的。
下面我们将映射一个双向关联(bi-directional association)- 在Java里面让person和event可以从关联的 任何一端访问另一端。当然,数据库表定义没有改变,我们仍然需要多对多(many-to-many)的阶数(multiplicity)。一个关系型数据库要比网络编程语言 更加灵活,所以它并不需要任何像导航方向(navigation direction)的东西 - 数据可以用任何可能的方式进行查看和获取。
首先,把一个参与者(person)的集合加入Event类中:
private Set participants = new HashSet(); public Set getParticipants() { return participants; } public void setParticipants(Set participants) { this.participants = participants; }
在Event.hbm.xml里面也映射这个关联。
<set name="participants" table="PERSON_EVENT" inverse="true"> <key column="EVENT_ID"/> <many-to-many column="PERSON_ID" class="Person"/> </set>
如你所见,2个映射文件里都有通常的set映射。注意key和many-to-many 里面的字段名在两个映射文件中是交换的。这里最重要的不同是Event映射文件里set元素的 inverse="true"参数。
这个表示Hibernate需要在两个实体间查找关联信息的时候,应该使用关联的另外一端 - Person类。 这将会极大的帮助你理解双向关联是如何在我们的两个实体间创建的。
首先,请牢记在心,Hibernate并不影响通常的Java语义。 在单向关联中,我们是怎样在一个Person和一个Event之间创建联系的? 我们把一个Event的实例加到一个Person类内的Event集合里。所以,显然如果我们要让这个关联可以双向工作, 我们需要在另外一端做同样的事情 - 把Person加到一个Event类内的Person集合中。 这“在关联的两端设置联系”是绝对必要的而且你永远不应该忘记做它。
许多开发者通过创建管理关联的方法来保证正确的设置了关联的两端,比如在Person里:
protected Set getEvents() { return events; } protected void setEvents(Set events) { this.events = events; } public void addToEvent(Event event) { this.getEvents().add(event); event.getParticipants().add(this); } public void removeFromEvent(Event event) { this.getEvents().remove(event); event.getParticipants().remove(this); }
注意现在对于集合的get和set方法的访问控制级别是protected - 这允许在位于同一个包(package)中的类以及继承自这个类的子类 可以访问这些方法,但是禁止其它的直接外部访问,避免了集合的内容出现混乱。你应该尽可能的在集合所对应的另外一端也这样做。
inverse映射参数究竟表示什么呢?对于你和对于Java来说,一个双向关联仅仅是在两端简单的设置引用。然而仅仅这样 Hibernate并没有足够的信息去正确的产生INSERT和UPDATE语句(以避免违反数据库约束), 所以Hibernate需要一些帮助来正确的处理双向关联。把关联的一端设置为inverse将告诉Hibernate忽略关联的 这一端,把这端看成是另外一端的一个镜子(mirror)。这就是Hibernate所需的信息,Hibernate用它来处理如何把把 一个数据导航模型映射到关系数据库表定义。 你仅仅需要记住下面这个直观的规则:所有的双向关联需要有一端被设置为inverse。在一个一对多(one-to-many)关联中 它必须是代表多(many)的那端。而在多对多(many-to-many)关联中,你可以任意选取一端,两端之间并没有差别。
这个教程覆盖了关于开发一个简单的Hibernate应用程序的几个基础方面。
如果你已经对Hibernate感到自信,继续浏览开发指南里你感兴趣的内容-那些会被问到的问题大多是事务处理 (第 12 章 事务和并发), 抓取(fetch)的效率 (第 20 章 提升性能 ),或者API的使用 (第 11 章 与对象共事)和查询的特性(第 11.4 节 “查询”)。
不要忘记去Hibernate的网站查看更多(有针对性的)教程。