这份教程解释如何在Apache Tomcat servlet容器中为web程序安装Hibernate 2.1。Hibernate在大多数主流J2EE应用服务器 的受管理环境中都可以良好运作,甚至也可以在独立Java应用程序中运行。在本例中使用的示例数据库系统是PostgreSQL 7.3,当然也可以 很容易的支持其他数据库,只需要修改Hibernate SQL方言的配置。
第一步,我们必须拷贝所有需要的运行库到Tomcat去。在这篇教程中,我们使用一个单独的web程序(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,Proxcool)。对于本教程来说,把pg73jdbc3.jar库文件(对应PostgreSQL 7.3和JDK 1.4)到全局类装载器路径去。如果你希望使用一个不同的数据库,拷贝相应的JDBC 驱动)。
不要拷贝任何其他东西到全局类装载器去。否则你可能在一些工具上遇到麻烦,比如log4j, commons-logging等。 记得要使用每个web应用程序自己的上下文类路径,就是说把你自己的类库拷贝到WEB-INF/lib下去,把配置文件configuration/property拷贝到WEB-INF/classes下面去。这两个目录默认都是上下文类路径级别的。
Hibernate本身打包成一个JAR库。hibernate2.jar文件要和你应用程序的其他库文件一起放在上下文类路径中。在运行时,Hibernate还需要一些第三方库,它们在Hibernate发行包的lib/目录下。参见表 1.1 “ Hibernate 第三方库 ”。把你需要的第三方库文件也拷贝到上下文类路径去。
表 1.1. Hibernate 第三方库
库 | 描述 |
---|---|
dom4j (必需) | Hibernate在解析XML配置和XML映射元文件时需要使用dom4j。 |
CGLIB (必需) | Hibernate在运行时使用这个代码生成库强化类(与Java反射机制联合使用)。 |
Commons Collections, Commons Logging (必需) | Hibernat使用Apache Jakarta Commons项目提供的多个工具类库。 |
ODMG4 (必需) | Hibernate提供了一个可选的ODMG兼容持久化管理界面。如果你需要映射集合,你就需要这个类库,就算你不是为了使用ODMG API。我们在这个教程中没有使用集合映射,但不管怎样把这个JAR拷贝过去总是不错的。 |
EHCache (必需) | Hibernate可以使用不同的第二级Cache方案。如果没有修改配置的话,EHCache提供默认的Cache。 |
Log4j (可选) | Hibernate使用Commons Logging API,后者可以使用Log4j作为底层实施log的机制。如果上下文类目录中存在Log4j库,Commons Logging就会使用Log4j和它在上下文类路径中找到的log4j.properties文件。在Hibernate发行包中包含有一个示例的properties文件。所以,如果你想看看幕布之后到底发生了什么,也把log4j.jar拷贝到你的上下文类路径去吧(它位于src/目录中)。 |
其他文件是不是必需的? | 请察看Hibernate发行包中的/lib/README.txt文件。这是一个Hibernate发行包中附带的第三方类库的列表,总是保持更新。你可以在那里找到所有必需或者可选的类库的列表。 |
我们现在来在Tomcat和Hibernate中配置共享的数据库连接池。也就是说Tomcat会提供经过池处理的JDBC连接(用它自己内置的DBCP连接池功能),Hibernate通过JNDI来请求这些连接。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目录。要访问任何Servlet,在你的浏览器中访问http://localhost:8080/quickstart就可以了(当然,在后面加上在你的web.xml文件中列出的servlet的名字)。你现在可以创建一个只有空的process()方法的简单servlet了。
Tomcat在这个配置下,使用DBCP连接池,通过JNDI位置:java:comp/env/jdbc/quickstart提供带有缓冲池的JDBCConnections。如果你在让连接池工作的时候遇到困难,请查阅Tomcat文档。如果你得到了JDBC驱动的exception信息,请先不要用Hibernate,测试JDBC连接池本身是否正确。Tomcat和JDBC的教程可以在Web上查到。
下一步是配置hibernate,来使用绑定到JNDI的连接池中提供的连接。我们使用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-2.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">net.sf.hibernate.dialect.PostgreSQLDialect</property> <!-- Mapping files --> <mapping resource="Cat.hbm.xml"/> </session-factory> </hibernate-configuration>
我们关闭了SQL命令的log,告诉Hibernate使用哪种SQL数据库方言(dialet),还有如何得到JDBC连接(通过Tomcat声明数据源池绑定的JNDI地址)。方言是必需的,因为不同的数据库都和SQL "标准"有一些出入。Hibernate会替你照管这些差异之处,发行包包含了所有主流的商业和开放源代码数据库的方言。
SessionFactory是Hibernate的概念,对应一个数据存储源,如果有多个数据库,可以创建多个XML配置文件,也在你的程序中创建多个Configuration和SessionFactory对象。
在hibernate.cfg.xml中的最后一个元素声明了Cat.hbm.xml是一个Hibernate XML映射文件,对应持久化类Cat。这个文件包含了把POJO类映射到数据库表(或多个数据库表)的元数据。我们稍后就回来看这个文件。让我们先编写这个POJO类,再在声明它的映射元数据。
Hibernate最好的使用方法是使用普通的Java对象(Plain Old Java Objects ,就是POJOs,有时候也称作Plain Ordinary Java Objects)这种编程模型来进行持久化。一个POJO很像JavaBean,属性通过getter和setter方法访问,对外隐藏了内部实现的细节。
package net.sf.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映射文件包含了对象/关系映射所需的元数据。元数据包含持久化类的声明和属性到数据库的映射(指向字段和其他实体的外键关联)。
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="net.sf.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的话,两只猫就是相同的。这个概念称为数据库标识。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)
你现在可以在你的数据库中首先创建这个表了,如果你需要使用SchemaExport工具把这个步骤自动化,请参阅第 15 章 工具箱指南。这个工具能够创建完整的SQL DDL,包括表定义,自定义的字段类型约束,惟一约束和索引。
我们现在可以开始Hibernate的Session了。它是持久化管理器接口,我们用它来从数据库中存取Cat。首先,我们要从SessionFactory中获取一个Session(Hibernate的工作单元)。
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
SessionFactory负责一个数据库,也只对应一个XML配置文件(hibernate.cfg.xml)。通过在创建 SessionFactory之前(它是不可变的),你可以访问Configuration来设置其他属性(甚至修改映射的元数据)。我们应该在哪儿创建SessionFactory,在我们的程序中又如何访问它呢? SessionFactory通常只是被初始化一次,比如通过一个load-on-startup servlet。这意味着你不应该在serlvet中把它作为一个实例变量来持有,应该放在其他地方。更深入一些的说,我们需要某种单例(Singleton)模式,我们才能更容易的在程序中访问SessionFactory。下面的方法同时解决了两个问题:配置和容易地访问SessionFactory。
我们实现一个HibernateUtil辅助类:
import net.sf.hibernate.*; import net.sf.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) { log.error("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(); session.set(s); } return s; } public static void closeSession() throws HibernateException { Session s = (Session) session.get(); session.set(null); if (s != null) s.close(); } }
这个类不但在它的静态属性中使用了SessionFactory,还使用了ThreadLocal来为当前工作线程保存Session。在你想用这个辅助类之前,请确保你理解了thread-local变量这个Java概念。
SessionFactory是线程安全的,很多线程可以同时访问它,获取Session。Session不是线程安全的,它代表与数据库之间的一次操作。Session通过SessionFactory打开,在所有的工作完成后,需要关闭:
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。你必须确保Session在你的数据库访问工作完成后关闭,不管是在你的servlet代码中,或者在servlet filter中,HTTP结果返回之前。后者的一个附带好处是,可以容易的使用延迟装载: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连接。