mirror of
https://github.com/hibernate/hibernate-orm
synced 2025-03-03 00:09:19 +00:00
git-svn-id: https://svn.jboss.org/repos/hibernate/core/branches/Branch_3_2@11779 1b8cb986-b30d-0410-93ca-fae66ebed9b2
1234 lines
62 KiB
XML
1234 lines
62 KiB
XML
<chapter id="tutorial">
|
||
<title>
|
||
Hibernate入门
|
||
</title>
|
||
<sect1 id="tutorial-intro" revision="1">
|
||
<title>
|
||
前言
|
||
</title>
|
||
|
||
<para>
|
||
本章是面向Hibernate初学者的一个入门教程。我们从一个使用驻留内存式(in-memory)数据库的简单命令行应用程序开始, 用易于理解的方式逐步开发。
|
||
|
||
</para>
|
||
|
||
<para>
|
||
本章面向Hibernate初学者,但需要Java和SQL知识。它是在Michael Goegl所写的指南的基础上完成的。在这里,我们称第三方库文件是指JDK 1.4和5.0。若使用JDK1.3,你可能需要其它的库文件。
|
||
</para>
|
||
|
||
<para>
|
||
本章的源代码已包含在发布包中,位于<literal>doc/reference/tutorial/</literal>目录下。
|
||
</para>
|
||
|
||
</sect1>
|
||
|
||
<sect1 id="tutorial-firstapp" revision="2">
|
||
<title>
|
||
第一部分 - 第一个Hibernate应用程序
|
||
</title>
|
||
<para>
|
||
首先我们将创建一个简单的基于控制台的(console-based)Hibernate应用程序。由于我们使用Java数据库(HSQL DB),所以不必安装任何数据库服务器。
|
||
</para>
|
||
|
||
<para>
|
||
假设我们希望有一个小应用程序可以保存我们希望参加的活动(events)和这些活动主办方的相关信息。
|
||
(译者注:在本教程的后面部分,我们将直接使用event而不是它的中文翻译“活动”,以免混淆。)
|
||
</para>
|
||
|
||
<para>
|
||
我们所做的第一件事就是创建我们的开发目录,并且把所有需要用到的Java库文件放进去。解压缩从Hibernate网站下载的Hibernate发布包,并把<literal>/lib</literal>目录下所有需要的库文件拷到我们新建开发目录下的<literal>/lib</literal>目录下。看起来就像这样:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[.
|
||
+lib
|
||
antlr.jar
|
||
cglib.jar
|
||
asm.jar
|
||
asm-attrs.jars
|
||
commons-collections.jar
|
||
commons-logging.jar
|
||
ehcache.jar
|
||
hibernate3.jar
|
||
jta.jar
|
||
dom4j.jar
|
||
log4j.jar ]]></programlisting>
|
||
|
||
<para>
|
||
<emphasis>到编写本文时为止</emphasis>,这些是Hibernate运行所需要的最小库文件集合(注意我们也拷贝了 Hibernate3.jar,这个是最主要的文件)。你正使用的Hibernate版本可能需要比这更多或少一些的库文件。请参见发布包中的<literal>lib/</literal>目录下的<literal>README.txt</literal>,以获取更多关于所需和可选的第三方库文件信息(事实上,Log4j并不是必须的库文件,但被许多开发者所喜欢)。
|
||
|
||
</para>
|
||
|
||
<para>
|
||
接下来我们创建一个类,用来代表那些我们希望储存在数据库里的event。
|
||
</para>
|
||
|
||
<sect2 id="tutorial-firstapp-firstclass" revision="1">
|
||
<title>
|
||
第一个class
|
||
</title>
|
||
|
||
<para>
|
||
我们的第一个持久化类是一个带有一些属性(property)的简单JavaBean类:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[package events;
|
||
|
||
import java.util.Date;
|
||
|
||
public class Event {
|
||
private Long id;
|
||
|
||
private String title;
|
||
private Date date;
|
||
|
||
public 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;
|
||
}
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
你可以看到这个类对属性的存取方法(getter and setter method)使用了标准JavaBean命名约定,同时把类属性(field)的访问级别设成私有的(private)。这是推荐的设计,但并不是必须的。Hibernate也可以直接访问这些field,而使用访问方法(accessor method)的好处是提供了重构时的健壮性(robustness)。为了通过反射机制(Reflection)来实例化这个类的对象,我们需要提供一个无参的构造器(no-argument constructor)。
|
||
</para>
|
||
|
||
<para>
|
||
对一特定的event, <literal>id</literal> 属性持有唯一的标识符(identifier)的值。如果我们希望使用Hibernate提供的所有特性,那么所有的持久化实体(persistent entity)类(这里也包括一些次要依赖类)都需要一个这样的标识符属性。而事实上,大多数应用程序(特别是web应用程序)都需要通过标识符来区别对象,所以你应该考虑使用标识符属性而不是把它当作一种限制。然而,我们通常不会操作对象的标识(identity),因此它的setter方法的访问级别应该声明private。这样当对象被保存的时候,只有Hibernate可以为它分配标识符值。你可看到Hibernate可以直接访问public,private和protected的访问方法和field。所以选择哪种方式完全取决于你,你可以使你的选择与你的应用程序设计相吻合。
|
||
</para>
|
||
|
||
<para>
|
||
所有的持久化类(persistent classes)都要求有无参的构造器,因为Hibernate必须使用Java反射机制来为你创建对象。构造器(constructor)的访问级别可以是private,然而当生成运行时代理(runtime proxy)的时候则要求使用至少是package 级别的访问控制,这样在没有字节码指令(bytecode instrumentation)的情况下,从持久化类里获取数据会更有效率。
|
||
</para>
|
||
|
||
<para>
|
||
把这个Java源代码文件放到开发目录下的<literal>src</literal>目录里,注意包位置要正确。 现在这个目录看起来应该像这样:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[.
|
||
+lib
|
||
<Hibernate and third-party libraries>
|
||
+src
|
||
+events
|
||
Event.java]]></programlisting>
|
||
|
||
<para>
|
||
下一步,我们把这个持久化类的信息告诉Hibernate。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-firstapp-mapping" revision="1">
|
||
<title>
|
||
映射文件
|
||
</title>
|
||
|
||
<para>
|
||
Hibernate需要知道怎样去加载(load)和存储(store)持久化类的对象。这正是Hibernate映射文件发挥作用的地方。映射文件告诉Hibernate它,应该访问数据库(database)里面的哪个表(table)及应该使用表里面的哪些字段(column)。
|
||
|
||
</para>
|
||
|
||
<para>
|
||
一个映射文件的基本结构看起来像这样:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<?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>]]></programlisting>
|
||
|
||
<para>
|
||
注意Hibernate的DTD是非常复杂的。你的编辑器或者IDE里使用它来自动完成那些用来映射的XML元素(element)和属性(attribute)。你也可以在文本编辑器里打开DTD-这是最简单的方式来概览所有的元素和attribute,并查看它们的缺省值以及注释。注意Hibernate不会从web加载DTD文件,但它会首先在应用程序的classpath中查找。DTD文件已包括在<literal>hibernate3.jar</literal>里,同时也在Hibernate发布包的<literal>src/</literal>目录下。
|
||
</para>
|
||
|
||
<para>
|
||
为缩短代码长度,在以后的例子里我们会省略DTD的声明。当然,在实际的应用程序中,DTD声明是必须的。
|
||
</para>
|
||
|
||
<para>
|
||
在<literal>hibernate-mapping</literal>标签(tag)之间, 含有一个<literal>class</literal>元素。所有的持久化实体类(再次声明,或许接下来会有依赖类,就是那些次要的实体)都需要一个这样的映射,来把类对象映射到SQL数据库里的表。
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<hibernate-mapping>
|
||
|
||
<class name="events.Event" table="EVENTS">
|
||
|
||
</class>
|
||
|
||
</hibernate-mapping>]]></programlisting>
|
||
|
||
<para>
|
||
到目前为止,我们告诉了Hibernate怎样把<literal>Events</literal>类的对象持久化到数据库的<literal>EVENTS</literal>表里,以及怎样从<literal>EVENTS</literal>表加载到<literal>Events</literal>类的对象。每个实例对应着数据库表中的一行。现在我们将继续讨论有关唯一标识符属性到数据库表的映射。另外,由于我们不关心怎样处理这个标识符,我们就配置由Hibernate的标识符生成策略来产生代理主键字段。
|
||
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<hibernate-mapping>
|
||
|
||
<class name="events.Event" table="EVENTS">
|
||
<id name="id" column="EVENT_ID">
|
||
<generator class="native"/>
|
||
</id>
|
||
</class>
|
||
|
||
</hibernate-mapping>]]></programlisting>
|
||
|
||
<para>
|
||
<literal>id</literal>元素是标识符属性的声明,<literal>name="id"</literal> 声明了Java属性的名字 - Hibernate会使用<literal>getId()</literal>和<literal>setId()</literal>来访问它。 <literal>column</literal>属性则告诉Hibernate, 我们使用<literal>EVENTS</literal>表的哪个字段作为主键。嵌套的<literal>generator</literal>元素指定了标识符生成策略,在这里我们指定<literal>native</literal>,它根据已配置的数据库(方言)自动选择最佳的标识符生成策略。Hibernate支持由数据库生成,全局唯一性(globally unique)和应用程序指定(或者你自己为任何已有策略所写的扩展)这些策略来生成标识符。
|
||
</para>
|
||
|
||
<para>
|
||
最后我们在映射文件里面包含需要持久化属性的声明。默认情况下,类里面的属性都被视为非持久化的:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[
|
||
<hibernate-mapping>
|
||
|
||
<class name="events.Event" table="EVENTS">
|
||
<id name="id" column="EVENT_ID">
|
||
<generator class="native"/>
|
||
</id>
|
||
<property name="date" type="timestamp" column="EVENT_DATE"/>
|
||
<property name="title"/>
|
||
</class>
|
||
|
||
</hibernate-mapping>]]></programlisting>
|
||
|
||
<para>
|
||
和<literal>id</literal>元素一样,<literal>property</literal>元素的<literal>name</literal>属性告诉Hibernate使用哪个getter和setter方法。在此例中,Hibernate会寻找<literal>getDate()/setDate()</literal>, 以及<literal>getTitle()/setTitle()</literal>。
|
||
</para>
|
||
|
||
<para>
|
||
为什么<literal>date</literal>属性的映射含有<literal>column</literal> attribute,而<literal>title</literal>却没有?当没有设定<literal>column</literal> attribute 的时候,Hibernate缺省地使用JavaBean的属性名作为字段名。对于<literal>title</literal>,这样工作得很好。然而,<literal>date</literal>在多数的数据库里,是一个保留关键字,所以我们最好把它映射成一个不同的名字。
|
||
|
||
</para>
|
||
|
||
<para>
|
||
另一有趣的事情是<literal>title</literal>属性缺少一个<literal>type</literal> attribute。我们在映射文件里声明并使用的类型,却不是我们期望的那样,是Java数据类型,同时也不是SQL数据库的数据类型。这些类型就是所谓的Hibernate 映射类型<emphasis>(mapping types)</emphasis>,它们能把Java数据类型转换到SQL数据类型,反之亦然。再次重申,如果在映射文件中没有设置<literal>type</literal>属性的话,Hibernate会自己试着去确定正确的转换类型和它的映射类型。在某些情况下这个自动检测机制(在Java 类上使用反射机制)不会产生你所期待或需要的缺省值。<literal>date</literal>属性就是个很好的例子,Hibernate无法知道这个属性(<literal>java.util.Date</literal>类型的)应该被映射成:SQL <literal>date</literal>,或<literal>timestamp</literal>,还是<literal>time</literal> 字段。在此例中,把这个属性映射成<literal>timestamp</literal> 转换器,这样我们预留了日期和时间的全部信息。
|
||
</para>
|
||
|
||
<para>
|
||
应该把这个映射文件保存为<literal>Event.hbm.xml</literal>,且就在<literal>Event</literal>Java类的源文件目录下。映射文件可随意地命名,但<literal>hbm.xml</literal>的后缀已成为Hibernate开发者社区的约定。现在目录结构看起来应该像这样:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[.
|
||
+lib
|
||
<Hibernate and third-party libraries>
|
||
+src
|
||
+events
|
||
Event.java
|
||
Event.hbm.xml]]></programlisting>
|
||
|
||
<para>
|
||
我们继续进行Hibernate的主要配置。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-firstapp-configuration" revision="2">
|
||
<title>
|
||
Hibernate配置
|
||
</title>
|
||
|
||
<para>
|
||
现在我们已经有了一个持久化类和它的映射文件,该是配置Hibernate的时候了。在此之前,我们需要一个数据库。 HSQL DB是种基于Java 的SQL数据库管理系统(DBMS),可以从HSQL DB的网站上下载。实际上,你只需下载的包中的<literal>hsqldb.jar</literal>文件,并把这个文件放在开发文件夹的<literal>lib/</literal>目录下即可。
|
||
|
||
</para>
|
||
|
||
<para>
|
||
在开发的根目录下创建一个<literal>data</literal>目录 - 这是HSQL DB存储数据文件的地方。此时在data目录中运行<literal>java -classpath ../lib/hsqldb.jar org.hsqldb.Server</literal>就可启动数据库。你可以在log中看到它的启动,及绑定到TCP/IP套结字,这正是我们的应用程序稍后会连接的地方。如果你希望在本例中运行一个全新的数据库,就在窗口中按下<literal>CTRL + C</literal>来关闭HSQL数据库,并删除<literal>data/</literal>目录下的所有文件,再重新启动HSQL数据库。
|
||
</para>
|
||
|
||
<para>
|
||
Hibernate是你的应用程序里连接数据库的那层,所以它需要连接用的信息。连接(connection)是通过一个也由我们配置的JDBC连接池(connection pool)来完成的。Hibernate的发布包里包含了许多开源的(open source)连接池,但在我们例子中使用Hibernate内置的连接池。注意,如果你希望使用一个产品级(production-quality)的第三方连接池软件,你必须拷贝所需的库文件到你的classpath下,并使用不同的连接池设置。
|
||
</para>
|
||
|
||
<para>
|
||
为了保存Hibernate的配置,我们可以使用一个简单的<literal>hibernate.properties</literal>文件,或者一个稍微复杂的<literal>hibernate.cfg.xml</literal>,甚至可以完全使用程序来配置Hibernate。多数用户更喜欢使用XML配置文件:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<?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:hsql://localhost</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>
|
||
|
||
<!-- Enable Hibernate's automatic session context management -->
|
||
<property name="current_session_context_class">thread</property>
|
||
|
||
<!-- Disable the second-level cache -->
|
||
<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</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="events/Event.hbm.xml"/>
|
||
|
||
</session-factory>
|
||
|
||
</hibernate-configuration>]]></programlisting>
|
||
|
||
<para>
|
||
注意这个XML配置使用了一个不同的DTD。在这里,我们配置了Hibernate的<literal>SessionFactory</literal>-一个关联于特定数据库全局的工厂(factory)。如果你要使用多个数据库,就要用多个的<literal><session-factory></literal>,通常把它们放在多个配置文件中(为了更容易启动)。
|
||
</para>
|
||
|
||
<para>
|
||
最开始的4个<literal>property</literal>元素包含必要的JDBC连接信息。方言(dialect)的<literal>property</literal>元素指明Hibernate 生成的特定SQL变量。你很快会看到,Hibernate对持久化上下文的自动session管理就会派上用场。 打开<literal>hbm2ddl.auto</literal>选项将自动生成数据库模式(schema)- 直接加入数据库中。当然这个选项也可以被关闭(通过去除这个配置选项)或者通过Ant任务<literal>SchemaExport</literal>的帮助来把数据库schema重定向到文件中。最后,在配置中为持久化类加入映射文件。
|
||
</para>
|
||
|
||
<para>
|
||
把这个文件拷贝到源代码目录下面,这样它就位于classpath的根目录的最后。Hibernate在启动时会自动在classpath的根目录查找名为<literal>hibernate.cfg.xml</literal>的配置文件。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-firstapp-ant" revision="1">
|
||
<title>
|
||
用Ant构建
|
||
</title>
|
||
|
||
<para>
|
||
现在我们用Ant来构建应用程序。你必须先安装Ant-可以从<ulink url="http://ant.apache.org/bindownload.cgi">Ant 下载页面</ulink>得到它。怎样安装Ant就不在这里介绍了,请参考<ulink url="http://ant.apache.org/manual/index.html">Ant 用户手册</ulink>。当你安装完了Ant,就可以开始创建<literal>build.xml</literal>文件,把它直接放在开发目录下面。
|
||
</para>
|
||
<para>
|
||
一个简单的build文件看起来像这样:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<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>]]></programlisting>
|
||
|
||
<para>
|
||
这将告诉Ant把所有在lib目录下以<literal>.jar</literal>结尾的文件拷贝到classpath中以供编译之用。它也把所有的非Java源代码文件,例如配置和Hibernate映射文件,拷贝到目标目录。如果你现在运行Ant,会得到以下输出:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[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 ]]></programlisting>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-firstapp-helpers" revision="3">
|
||
<title>
|
||
启动和辅助类
|
||
</title>
|
||
|
||
|
||
<para>
|
||
是时候来加载和储存一些<literal>Event</literal>对象了,但首先我们得编写一些基础的代码以完成设置。我们必须启动Hibernate,此过程包括创建一个全局的<literal>SessoinFactory</literal>,并把它储存在应用程序代码容易访问的地方。<literal>SessionFactory</literal>可以创建并打开新的<literal>Session</literal>。一个<literal>Session</literal>代表一个单线程的单元操作,<literal>SessionFactory</literal>则是个线程安全的全局对象,只需要被实例化一次。
|
||
</para>
|
||
|
||
<para>
|
||
我们将创建一个<literal>HibernateUtil</literal>辅助类(helper class)来负责启动Hibernate和更方便地操作<literal>SessionFactory</literal>。让我们来看一下它的实现:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[package util;
|
||
|
||
import org.hibernate.*;
|
||
import org.hibernate.cfg.*;
|
||
|
||
public class HibernateUtil {
|
||
|
||
private 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 SessionFactory getSessionFactory() {
|
||
return sessionFactory;
|
||
}
|
||
|
||
}]]></programlisting>
|
||
|
||
|
||
<para>
|
||
这个类不但在它的静态初始化过程(仅当加载这个类的时候被JVM执行一次)中产生全局的<literal>SessionFactory</literal>,而且隐藏了它使用了静态singleton的事实。它也可能在应用程序服务器中的JNDI查找<literal>SessionFactory</literal>。
|
||
</para>
|
||
|
||
<para>
|
||
如果你在配置文件中给<literal>SessionFactory</literal>一个名字,在<literal>SessionFactory</literal>创建后,Hibernate会试着把它绑定到JNDI。要完全避免这样的代码,你也可以使用JMX部署,让具有JMX能力的容器来实例化<literal>HibernateService</literal>并把它绑定到JNDI。这些高级可选项在后面的章节中会讨论到。
|
||
</para>
|
||
|
||
<para>
|
||
把<literal>HibernateUtil.java</literal>放在开发目录的源代码路径下,与放<literal>events</literal>的包并列:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[.
|
||
+lib
|
||
<Hibernate and third-party libraries>
|
||
+src
|
||
+events
|
||
Event.java
|
||
Event.hbm.xml
|
||
+util
|
||
HibernateUtil.java
|
||
hibernate.cfg.xml
|
||
+data
|
||
build.xml]]></programlisting>
|
||
|
||
|
||
<para>
|
||
再次编译这个应用程序应该不会有问题。最后我们需要配置一个日志(logging)系统 - Hibernate使用通用日志接口,允许你在Log4j和JDK 1.4 日志之间进行选择。多数开发者更喜欢Log4j:从Hibernate的发布包中(它在<literal>etc/</literal>目录下)拷贝<literal>log4j.properties</literal>到你的<literal>src</literal>目录,与<literal>hibernate.cfg.xml</literal>.放在一起。看一下配置示例,如果你希望看到更加详细的输出信息,你可以修改配置。默认情况下,只有Hibernate的启动信息才会显示在标准输出上。
|
||
</para>
|
||
|
||
<para>
|
||
示例的基本框架完成了 - 现在我们可以用Hibernate来做些真正的工作。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-firstapp-workingpersistence" revision="5">
|
||
<title>
|
||
加载并存储对象
|
||
</title>
|
||
|
||
<para>
|
||
我们终于可以使用Hibernate来加载和存储对象了,编写一个带有<literal>main()</literal>方法的<literal>EventManager</literal>类:
|
||
|
||
</para>
|
||
|
||
<programlisting><![CDATA[package events;
|
||
import org.hibernate.Session;
|
||
|
||
import java.util.Date;
|
||
|
||
import util.HibernateUtil;
|
||
|
||
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.getSessionFactory().close();
|
||
}
|
||
|
||
private void createAndStoreEvent(String title, Date theDate) {
|
||
|
||
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
|
||
|
||
session.beginTransaction();
|
||
|
||
Event theEvent = new Event();
|
||
theEvent.setTitle(title);
|
||
theEvent.setDate(theDate);
|
||
|
||
session.save(theEvent);
|
||
|
||
session.getTransaction().commit();
|
||
}
|
||
|
||
}]]></programlisting>
|
||
|
||
|
||
<para>
|
||
我们创建了个新的<literal>Event</literal>对象并把它传递给Hibernate。现在Hibernate负责与SQL打交道,并把<literal>INSERT</literal>命令传给数据库。在运行之前,让我们看一下处理<literal>Session</literal>和<literal>Transaction</literal>的代码。
|
||
|
||
</para>
|
||
<para>
|
||
一个<literal>Session</literal>就是个单一的工作单元。我们暂时让事情简单一些,并假设Hibernate<literal>Session</literal>和数据库事务是一一对应的。为了让我们的代码从底层的事务系统中脱离出来(此例中是JDBC,但也可能是JTA),我们使用Hibernate <literal>Session</literal>中的<literal>Transaction</literal> API。
|
||
</para>
|
||
|
||
<para>
|
||
<literal>sessionFactory.getCurrentSession()</literal>是干什么的呢?首先,只要你持有<literal>SessionFactory</literal>(幸亏我们有<literal>HibernateUtil</literal>,可以随时获得),大可在任何时候、任何地点调用这个方法。<literal>getCurrentSession()</literal>方法总会返回“当前的”工作单元。记得我们在<literal>hibernate.cfg.xml</literal>中把这一配置选项调整为"thread"了吗?因此,因此,当前工作单元被绑定到当前执行我们应用程序的Java线程。但是,这并非是完全准确的,你还得考虑工作单元的生命周期范围 (scope),它何时开始,又何时结束.
|
||
</para>
|
||
|
||
<para>
|
||
<literal>Session</literal>在第一次被使用的时候,即第一次调用<literal>getCurrentSession()</literal>的时候,其生命周期就开始。然后它被Hibernate绑定到当前线程。当事务结束的时候,不管是提交还是回滚,Hibernate会自动把<literal>Session</literal>从当前线程剥离,并且关闭它。假若你再次调用<literal>getCurrentSession()</literal>,你会得到一个新的<literal>Session</literal>,并且开始一个新的工作单元。这种<emphasis>线程绑定(thread-bound)</emphasis>的编程模型(model)是使用Hibernate的最广泛的方式,因为它支持对你的代码灵活分层(事务划分可以和你的数据访问代码分离开来,在本教程的后面部分就会这么做)。
|
||
|
||
</para>
|
||
|
||
<para>
|
||
|
||
和工作单元的生命周期这个话题相关,Hibernate <literal>Session</literal>是否被应该用来执行多次数据库操作?上面的例子对每一次操作使用了一个<literal>Session</literal>,这完全是巧合,这个例子不是很复杂,无法展示其他方式。Hibernate <literal>Session</literal>的生命周期可以很灵活,但是你绝不要把你的应用程序设计成为<emphasis>每一次</emphasis>数据库操作都用一个新的Hibernate <literal>Session</literal>。因此就算下面的例子(它们都很简单)中你可以看到这种用法,记住<emphasis>每次操作一个session</emphasis>是一个反模式。在本教程的后面会展示一个真正的(web)程序。
|
||
</para>
|
||
|
||
|
||
|
||
<para>
|
||
关于事务处理及事务边界界定的详细信息,请参看<xref linkend="transactions"/>。在上面的例子中,我们也忽略了所有的错误与回滚的处理。
|
||
</para>
|
||
|
||
<para>
|
||
为第一次运行我们的程序,我们得在Ant的build文件中增加一个可以调用得到的target。
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<target name="run" depends="compile">
|
||
<java fork="true" classname="events.EventManager" classpathref="libraries">
|
||
<classpath path="${targetdir}"/>
|
||
<arg value="${action}"/>
|
||
</java>
|
||
</target>]]></programlisting>
|
||
|
||
<para>
|
||
<literal>action</literal>参数(argument)的值是通过命令行调用这个target的时候设置的:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[C:\hibernateTutorial\>ant run -Daction=store]]></programlisting>
|
||
|
||
<para>
|
||
你应该会看到,编译以后,Hibernate根据你的配置启动,并产生一大堆的输出日志。在日志最后你会看到下面这行:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)]]></programlisting>
|
||
|
||
<para>
|
||
这是Hibernate执行的<literal>INSERT</literal>命令,问号代表JDBC的绑定参数。如果想要看到绑定参数的值或者减少日志的长度,就要调整你在<literal>log4j.properties</literal>文件里的设置。
|
||
</para>
|
||
|
||
<para>
|
||
我们想要列出所有已经被存储的events,就要增加一个条件分支选项到main方法中去。
|
||
</para>
|
||
|
||
<programlisting><![CDATA[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());
|
||
}
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
我们也增加一个新的<literal>listEvents()</literal>方法:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[private List listEvents() {
|
||
|
||
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
|
||
|
||
session.beginTransaction();
|
||
|
||
List result = session.createQuery("from Event").list();
|
||
|
||
session.getTransaction().commit();
|
||
|
||
return result;
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
我们在这里是用一个HQL(Hibernate Query Language-Hibernate查询语言)查询语句来从数据库中加载所有存在的<literal>Event</literal>对象。Hibernate会生成适当的SQL,把它发送到数据库,并操作从查询得到数据的<literal>Event</literal>对象。当然,你可以使用HQL来创建更加复杂的查询。
|
||
</para>
|
||
|
||
<para>
|
||
现在,根据以下步骤来执行并测试以上各项:
|
||
</para>
|
||
|
||
<itemizedlist>
|
||
<listitem>
|
||
<para>
|
||
运行<literal>ant run -Daction=store</literal>来保存一些内容到数据库。当然,先得用hbm2ddl来生成数据库schema。
|
||
</para>
|
||
</listitem>
|
||
<listitem>
|
||
<para>
|
||
现在把<literal>hibernate.cfg.xml</literal>文件中hbm2ddl属性注释掉,这样我们就取消了在启动时用hbm2ddl来生成数据库schema。通常只有在不断重复进行单元测试的时候才需要打开它,但再次运行hbm2ddl会把你保存的一切都删掉(<emphasis>drop</emphasis>)——<literal>create</literal>配置的真实含义是:“在创建SessionFactory的时候,从schema 中drop 掉所有的表,再重新创建它们”。
|
||
</para>
|
||
</listitem>
|
||
</itemizedlist>
|
||
|
||
|
||
<para>
|
||
如果你现在使用命令行参数<literal>-Daction=list</literal>运行Ant,你会看到那些至今为止我们所储存的events。当然,你也可以多调用几次<literal>store</literal>以保存更多的envents。
|
||
</para>
|
||
|
||
<para>
|
||
注意,很多Hibernate新手在这一步会失败,我们不时看到关于<emphasis>Table not found</emphasis>错误信息的提问。但是,只要你根据上面描述的步骤来执行,就不会有这个问题,因为hbm2ddl会在第一次运行的时候创建数据库schema,后继的应用程序重起后还能继续使用这个schema。假若你修改了映射,或者修改了数据库schema,你必须把hbm2ddl重新打开一次。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
</sect1>
|
||
<sect1 id="tutorial-associations">
|
||
<title>
|
||
第二部分 - 关联映射
|
||
</title>
|
||
|
||
<para>
|
||
我们已经映射了一个持久化实体类到表上。让我们在这个基础上增加一些类之间的关联。首先我们往应用程序里增加人(people)的概念,并存储他们所参与的一个Event列表。(译者注:与Event一样,我们在后面将直接使用person来表示“人”而不是它的中文翻译)
|
||
</para>
|
||
|
||
<sect2 id="tutorial-associations-mappinguser" revision="1">
|
||
<title>
|
||
映射Person类
|
||
</title>
|
||
|
||
<para>
|
||
最初简单的<literal>Person</literal>类:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[package events;
|
||
|
||
public class Person {
|
||
|
||
private Long id;
|
||
private int age;
|
||
private String firstname;
|
||
private String lastname;
|
||
|
||
public Person() {}
|
||
|
||
// Accessor methods for all properties, private setter for 'id'
|
||
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
创建一个名为<literal>Person.hbm.xml</literal>的新映射文件(别忘了最上面的DTD引用):
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<hibernate-mapping>
|
||
|
||
<class name="events.Person" table="PERSON">
|
||
<id name="id" column="PERSON_ID">
|
||
<generator class="native"/>
|
||
</id>
|
||
<property name="age"/>
|
||
<property name="firstname"/>
|
||
<property name="lastname"/>
|
||
</class>
|
||
|
||
</hibernate-mapping>]]></programlisting>
|
||
|
||
<para>
|
||
最后,把新的映射加入到Hibernate的配置中:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<mapping resource="events/Event.hbm.xml"/>
|
||
<mapping resource="events/Person.hbm.xml"/>]]></programlisting>
|
||
|
||
<para>
|
||
现在我们在这两个实体之间创建一个关联。显然,persons可以参与一系列events,而events也有不同的参加者(persons)。我们需要处理的设计问题是关联方向(directionality),阶数(multiplicity)和集合(collection)的行为。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-associations-unidirset" revision="3">
|
||
<title>
|
||
单向Set-based的关联
|
||
</title>
|
||
|
||
<para>
|
||
我们将向<literal>Person</literal>类增加一连串的events。那样,通过调用<literal>aPerson.getEvents()</literal>,就可以轻松地导航到特定person所参与的events,而不用去执行一个显式的查询。我们使用Java的集合类(collection):<literal>Set</literal>,因为set 不包含重复的元素及与我们无关的排序。
|
||
</para>
|
||
|
||
<para>
|
||
我们需要用set 实现一个单向多值关联。让我们在Java类里为这个关联编码,接着映射它:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[public class Person {
|
||
|
||
private Set events = new HashSet();
|
||
|
||
public Set getEvents() {
|
||
return events;
|
||
}
|
||
|
||
public void setEvents(Set events) {
|
||
this.events = events;
|
||
}
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
在映射这个关联之前,先考虑一下此关联的另外一端。很显然,我们可以保持这个关联是单向的。或者,我们可以在<literal>Event</literal>里创建另外一个集合,如果希望能够双向地导航,如:<literal>anEvent.getParticipants()</literal>。从功能的角度来说,这并不是必须的。因为你总可以显式地执行一个查询,以获得某个特定event的所有参与者。这是个在设计时需要做出的选择,完全由你来决定,但此讨论中关于关联的阶数是清楚的:即两端都是“多”值的,我们把它叫做<emphasis>多对多(many-to-many)</emphasis>关联。因而,我们使用Hibernate的多对多映射:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<class name="events.Person" table="PERSON">
|
||
<id name="id" column="PERSON_ID">
|
||
<generator class="native"/>
|
||
</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="events.Event"/>
|
||
</set>
|
||
|
||
</class>]]></programlisting>
|
||
|
||
<para>
|
||
Hibernate支持各种各样的集合映射,<literal><set></literal>使用的最为普遍。对于多对多关联(或叫<emphasis>n:m</emphasis>实体关系), 需要一个关联表(association table)。<literal>表</literal>里面的每一行代表从person到event的一个关联。表名是由<literal>set</literal>元素的<literal>table</literal>属性配置的。关联里面的标识符字段名,对于person的一端,是由<literal><key></literal>元素定义,而event一端的字段名是由<literal><many-to-many></literal>元素的<literal>column</literal>属性定义。你也必须告诉Hibernate集合中对象的类(也就是位于这个集合所代表的关联另外一端的类)。
|
||
</para>
|
||
|
||
<para>
|
||
因而这个映射的数据库schema是:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[
|
||
_____________ __________________
|
||
| | | | _____________
|
||
| EVENTS | | PERSON_EVENT | | |
|
||
|_____________| |__________________| | PERSON |
|
||
| | | | |_____________|
|
||
| *EVENT_ID | <--> | *EVENT_ID | | |
|
||
| EVENT_DATE | | *PERSON_ID | <--> | *PERSON_ID |
|
||
| TITLE | |__________________| | AGE |
|
||
|_____________| | FIRSTNAME |
|
||
| LASTNAME |
|
||
|_____________|
|
||
]]></programlisting>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-associations-working" revision="2">
|
||
<title>
|
||
使关联工作
|
||
</title>
|
||
|
||
<para>
|
||
我们把一些people和events 一起放到<literal>EventManager</literal>的新方法中:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[private void addPersonToEvent(Long personId, Long eventId) {
|
||
|
||
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
|
||
session.beginTransaction();
|
||
|
||
Person aPerson = (Person) session.load(Person.class, personId);
|
||
Event anEvent = (Event) session.load(Event.class, eventId);
|
||
|
||
aPerson.getEvents().add(anEvent);
|
||
|
||
session.getTransaction().commit();
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
在加载一<literal>Person</literal>和<literal>Event</literal>后,使用普通的集合方法就可容易地修改我们定义的集合。如你所见,没有显式的<literal>update()</literal>或<literal>save()</literal>,Hibernate会自动检测到集合已经被修改并需要更新回数据库。这叫做自动脏检查(<emphasis>automatic dirty checking</emphasis>),你也可以尝试修改任何对象的name或者date属性,只要他们处于<emphasis>持久化</emphasis>状态,也就是被绑定到某个Hibernate 的<literal>Session</literal>上(如:他们刚刚在一个单元操作被加载或者保存),Hibernate监视任何改变并在后台隐式写的方式执行SQL。同步内存状态和数据库的过程,通常只在单元操作结束的时候发生,称此过程为清理缓存<emphasis>(flushing)</emphasis>。在我们的代码中,工作单元由数据库事务的提交(或者回滚)来结束——这是由<literal>CurrentSessionContext</literal>类的<literal>thread</literal>配置选项定义的。
|
||
</para>
|
||
|
||
<para>
|
||
当然,你也可以在不同的单元操作里面加载person和event。或在<literal>Session</literal>以外修改不是处在持久化(persistent)状态下的对象(如果该对象以前曾经被持久化,那么我们称这个状态为<emphasis>脱管(detached)</emphasis>)。你甚至可以在一个集合被脱管时修改它:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[private void addPersonToEvent(Long personId, Long eventId) {
|
||
|
||
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
|
||
session.beginTransaction();
|
||
|
||
Person aPerson = (Person) session
|
||
.createQuery("select p from Person p left join fetch p.events where p.id = :pid")
|
||
.setParameter("pid", personId)
|
||
.uniqueResult(); // Eager fetch the collection so we can use it detached
|
||
|
||
Event anEvent = (Event) session.load(Event.class, eventId);
|
||
|
||
session.getTransaction().commit();
|
||
|
||
// End of first unit of work
|
||
|
||
aPerson.getEvents().add(anEvent); // aPerson (and its collection) is detached
|
||
|
||
// Begin second unit of work
|
||
|
||
Session session2 = HibernateUtil.getSessionFactory().getCurrentSession();
|
||
session2.beginTransaction();
|
||
|
||
session2.update(aPerson); // Reattachment of aPerson
|
||
|
||
session2.getTransaction().commit();
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
对<literal>update</literal>的调用使一个脱管对象重新持久化,你可以说它被绑定到一个新的单元操作上,所以在脱管状态下对它所做的任何修改都会被保存到数据库里。这也包括你对这个实体对象的集合所作的任何改动(增加/删除)。
|
||
</para>
|
||
|
||
<para>
|
||
这对我们当前的情形不是很有用,但它是非常重要的概念,你可以把它融入到你自己的应用程序设计中。在<literal>EventManager</literal>的main方法中添加一个新的动作,并从命令行运行它来完成我们所做的练习。如果你需要person及event的标识符 — 那就用<literal>save()</literal>方法返回它(你可能需要修改前面的一些方法来返回那个标识符):
|
||
</para>
|
||
|
||
<programlisting><![CDATA[else if (args[0].equals("addpersontoevent")) {
|
||
Long eventId = mgr.createAndStoreEvent("My Event", new Date());
|
||
Long personId = mgr.createAndStorePerson("Foo", "Bar");
|
||
mgr.addPersonToEvent(personId, eventId);
|
||
System.out.println("Added person " + personId + " to event " + eventId);
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
上面是个关于两个同等重要的实体类间关联的例子。像前面所提到的那样,在特定的模型中也存在其它的类和类型,这些类和类型通常是“次要的”。你已看到过其中的一些,像<literal>int</literal>或<literal>String</literal>。我们称这些类为<emphasis>值类型(value type)</emphasis>,它们的实例<emphasis>依赖(depend)</emphasis>在某个特定的实体上。这些类型的实例没有它们自己的标识(identity),也不能在实体间被共享(比如,两个person不能引用同一个<literal>firstname</literal>对象,即使他们有相同的first name)。当然,值类型并不仅仅在JDK中存在(事实上,在一个Hibernate应用程序中,所有的JDK类都被视为值类型),而且你也可以编写你自己的依赖类,例如<literal>Address</literal>,<literal>MonetaryAmount</literal>。
|
||
</para>
|
||
|
||
<para>
|
||
你也可以设计一个值类型的集合,这在概念上与引用其它实体的集合有很大的不同,但是在Java里面看起来几乎是一样的。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-associations-valuecollections">
|
||
<title>
|
||
值类型的集合
|
||
</title>
|
||
|
||
<para>
|
||
我们把一个值类型对象的集合加入<literal>Person</literal>实体中。我们希望保存email地址,所以使用<literal>String</literal>类型,而且这次的集合类型又是<literal>Set</literal>:
|
||
</para>
|
||
<programlisting><![CDATA[private Set emailAddresses = new HashSet();
|
||
|
||
public Set getEmailAddresses() {
|
||
return emailAddresses;
|
||
}
|
||
|
||
public void setEmailAddresses(Set emailAddresses) {
|
||
this.emailAddresses = emailAddresses;
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
这个<literal>Set</literal>的映射
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<set name="emailAddresses" table="PERSON_EMAIL_ADDR">
|
||
<key column="PERSON_ID"/>
|
||
<element type="string" column="EMAIL_ADDR"/>
|
||
</set>]]></programlisting>
|
||
|
||
<para>
|
||
比较这次和此前映射的差别,主要在于<literal>element</literal>部分,这次并没有包含对其它实体引用的集合,而是元素类型为<literal>String</literal>的集合(在映射中使用小写的名字”string“是向你表明它是一个Hibernate的映射类型或者类型转换器)。和之前一样,<literal>set</literal>元素的<literal>table</literal>属性决定了用于集合的表名。<literal>key</literal>元素定义了在集合表中外键的字段名。<literal>element</literal>元素的<literal>column</literal>属性定义用于实际保存<literal>String</literal>值的字段名。
|
||
</para>
|
||
|
||
<para>
|
||
看一下修改后的数据库schema。
|
||
</para>
|
||
|
||
<programlisting><![CDATA[
|
||
_____________ __________________
|
||
| | | | _____________
|
||
| 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 |
|
||
|_____________|
|
||
]]></programlisting>
|
||
|
||
<para>
|
||
你可以看到集合表的主键实际上是个复合主键,同时使用了2个字段。这也暗示了对于同一个person不能有重复的email地址,这正是Java里面使用Set时候所需要的语义(Set里元素不能重复)。
|
||
</para>
|
||
|
||
<para>
|
||
你现在可以试着把元素加入到这个集合,就像我们在之前关联person和event的那样。其实现的Java代码是相同的:
|
||
</para>
|
||
<programlisting><![CDATA[private void addEmailToPerson(Long personId, String emailAddress) {
|
||
|
||
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
|
||
session.beginTransaction();
|
||
|
||
Person aPerson = (Person) session.load(Person.class, personId);
|
||
|
||
// The getEmailAddresses() might trigger a lazy load of the collection
|
||
aPerson.getEmailAddresses().add(emailAddress);
|
||
|
||
session.getTransaction().commit();
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
这次我们没有使用<emphasis>fetch</emphasis>查询来初始化集合。因此,调用其getter方法会触发另一附加的select来初始化集合,这样我们才能把元素添加进去。检查SQL log,试着通过预先抓取来优化它。
|
||
</para>
|
||
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-associations-bidirectional" revision="1">
|
||
<title>
|
||
双向关联
|
||
</title>
|
||
|
||
<para>
|
||
接下来我们将映射双向关联(bi-directional association)- 在Java里让person和event可以从关联的任何一端访问另一端。当然,数据库schema没有改变,我们仍然需要多对多的阶数。一个关系型数据库要比网络编程语言 更加灵活,所以它并不需要任何像导航方向(navigation direction)的东西 - 数据可以用任何可能的方式进行查看和获取。
|
||
</para>
|
||
|
||
<para>
|
||
首先,把一个参与者(person)的集合加入<literal>Event</literal>类中:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[private Set participants = new HashSet();
|
||
|
||
public Set getParticipants() {
|
||
return participants;
|
||
}
|
||
|
||
public void setParticipants(Set participants) {
|
||
this.participants = participants;
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
在<literal>Event.hbm.xml</literal>里面也映射这个关联。
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<set name="participants" table="PERSON_EVENT" inverse="true">
|
||
<key column="EVENT_ID"/>
|
||
<many-to-many column="PERSON_ID" class="events.Person"/>
|
||
</set>]]></programlisting>
|
||
<para>
|
||
如你所见,两个映射文件里都有普通的<literal>set</literal>映射。注意在两个映射文件中,互换了<literal>key</literal>和<literal>many-to-many</literal>的字段名。这里最重要的是<literal>Event</literal>映射文件里增加了<literal>set</literal>元素的<literal>inverse="true"</literal>属性。
|
||
</para>
|
||
|
||
<para>
|
||
这意味着在需要的时候,Hibernate能在关联的另一端 - <literal>Person</literal>类得到两个实体间关联的信息。这将会极大地帮助你理解双向关联是如何在两个实体间被创建的。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-associations-usingbidir">
|
||
<title>
|
||
使双向连起来
|
||
</title>
|
||
|
||
<para>
|
||
首先请记住,Hibernate并不影响通常的Java语义。 在单向关联的例子中,我们是怎样在<literal>Person</literal>和<literal>Event</literal>之间创建联系的?我们把<literal>Event</literal>实例添加到<literal>Person</literal>实例内的event引用集合里。因此很显然,如果我们要让这个关联可以双向地工作,我们需要在另外一端做同样的事情 - 把<literal>Person</literal>实例加入<literal>Event</literal>类内的Person引用集合。这“在关联的两端设置联系”是完全必要的而且你都得这么做。
|
||
</para>
|
||
|
||
<para>
|
||
许多开发人员防御式地编程,创建管理关联的方法来保证正确的设置了关联的两端,比如在<literal>Person</literal>里:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[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);
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
注意现在对于集合的get和set方法的访问级别是protected - 这允许在位于同一个包(package)中的类以及继承自这个类的子类可以访问这些方法,但禁止其他任何人的直接访问,避免了集合内容的混乱。你应尽可能地在另一端也把集合的访问级别设成protected。
|
||
</para>
|
||
|
||
<para>
|
||
<literal>inverse</literal>映射属性究竟表示什么呢?对于你和Java来说,一个双向关联仅仅是在两端简单地正确设置引用。然而,Hibernate并没有足够的信息去正确地执行<literal>INSERT</literal>和<literal>UPDATE</literal>语句(以避免违反数据库约束),所以它需要一些帮助来正确的处理双向关联。把关联的一端设置为<literal>inverse</literal>将告诉Hibernate忽略关联的这一端,把这端看成是另外一端的一个<emphasis>镜象(mirror)</emphasis>。这就是所需的全部信息,Hibernate利用这些信息来处理把一个有向导航模型转移到数据库schema时的所有问题。你只需要记住这个直观的规则:所有的双向关联需要有一端被设置为<literal>inverse</literal>。在一对多关联中它必须是代表多(many)的那端。而在多对多(many-to-many)关联中,你可以任意选取一端,因为两端之间并没有差别。
|
||
</para>
|
||
</sect2>
|
||
<para>
|
||
让我们把进入一个小型的web应用程序。
|
||
</para>
|
||
|
||
</sect1>
|
||
|
||
<sect1 id="tutorial-webapp">
|
||
<title>第三部分 - EventManager web应用程序</title>
|
||
|
||
<para>
|
||
Hibernate web应用程序使用<literal>Session</literal> 和<literal>Transaction</literal>的方式几乎和独立应用程序是一样的。但是,有一些常见的模式(pattern)非常有用。现在我们编写一个<literal>EventManagerServlet</literal>。这个servlet可以列出数据库中保存的所有的events,还提供一个HTML表单来增加新的events。
|
||
</para>
|
||
|
||
<sect2 id="tutorial-webapp-servlet" revision="2">
|
||
<title>编写基本的servlet</title>
|
||
|
||
<para>
|
||
在你的源代码目录的<literal>events</literal>包中创建一个新的类:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[package events;
|
||
|
||
// Imports
|
||
|
||
public class EventManagerServlet extends HttpServlet {
|
||
|
||
// Servlet code
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
我们后面会用到<literal>dateFormatter</literal> 的工具, 它把<literal>Date</literal>对象转换为字符串。只要一个formatter作为servlet的成员就可以了。
|
||
</para>
|
||
|
||
<para>
|
||
这个servlet只处理 HTTP <literal>GET</literal> 请求,因此,我们要实现的是<literal>doGet()</literal>方法:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[protected void doGet(HttpServletRequest request,
|
||
HttpServletResponse response)
|
||
throws ServletException, IOException {
|
||
|
||
SimpleDateFormat dateFormatter = new SimpleDateFormat("dd.MM.yyyy");
|
||
|
||
try {
|
||
// Begin unit of work
|
||
HibernateUtil.getSessionFactory()
|
||
.getCurrentSession().beginTransaction();
|
||
|
||
// Process request and render page...
|
||
|
||
// End unit of work
|
||
HibernateUtil.getSessionFactory()
|
||
.getCurrentSession().getTransaction().commit();
|
||
|
||
} catch (Exception ex) {
|
||
HibernateUtil.getSessionFactory()
|
||
.getCurrentSession().getTransaction().rollback();
|
||
throw new ServletException(ex);
|
||
}
|
||
|
||
}]]></programlisting>
|
||
<para>
|
||
我们称这里应用的模式为每次请求一个session<emphasis>(session-per-request)</emphasis>。当有请求到达这个servlet的时候,通过对<literal>SessionFactory</literal>的第一次调用,打开一个新的Hibernate <literal>Session</literal>。然后启动一个数据库事务—所有的数据访问都是在事务中进行,不管是读还是写(我们在应用程序中不使用auto-commit模式)。
|
||
</para>
|
||
|
||
<para>
|
||
|
||
<emphasis>不要</emphasis>为每次数据库操作都使用一个新的Hibernate <literal>Session</literal>。将Hibernate <literal>Session</literal>的范围设置为整个请求。要用<literal>getCurrentSession()</literal>,这样它自动会绑定到当前Java线程。
|
||
</para>
|
||
|
||
<para>
|
||
下一步,对请求的可能动作进行处理,渲染出反馈的HTML。我们很快就会涉及到那部分。
|
||
</para>
|
||
|
||
<para>
|
||
最后,当处理与渲染都结束的时候,这个工作单元就结束了。假若在处理或渲染的时候有任何错误发生,会抛出一个异常,回滚数据库事务。这样,<literal>session-per-request</literal>模式就完成了。为了避免在每个servlet中都编写事务边界界定的代码,可以考虑写一个servlet 过滤器(filter)来更好地解决。关于这一模式的更多信息,请参阅Hibernate网站和Wiki,这一模式叫做<emphasis>Open Session in View</emphasis>—只要你考虑用JSP来渲染你的视图(view),而不是在servlet中,你就会很快用到它。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-webapp-processing" revision="1">
|
||
<title>处理与渲染</title>
|
||
|
||
<para>
|
||
我们来实现处理请求以及渲染页面的工作。
|
||
</para>
|
||
|
||
<programlisting><![CDATA[// Write HTML header
|
||
PrintWriter out = response.getWriter();
|
||
out.println("<html><head><title>Event Manager</title></head><body>");
|
||
|
||
// Handle actions
|
||
if ( "store".equals(request.getParameter("action")) ) {
|
||
|
||
String eventTitle = request.getParameter("eventTitle");
|
||
String eventDate = request.getParameter("eventDate");
|
||
|
||
if ( "".equals(eventTitle) || "".equals(eventDate) ) {
|
||
out.println("<b><i>Please enter event title and date.</i></b>");
|
||
} else {
|
||
createAndStoreEvent(eventTitle, dateFormatter.parse(eventDate));
|
||
out.println("<b><i>Added event.</i></b>");
|
||
}
|
||
}
|
||
|
||
// Print page
|
||
printEventForm(out);
|
||
listEvents(out, dateFormatter);
|
||
|
||
// Write HTML footer
|
||
out.println("</body></html>");
|
||
out.flush();
|
||
out.close();]]></programlisting>
|
||
<para>
|
||
<literal>listEvents()</literal>方法使用绑定到当前线程的Hibernate <literal>Session</literal>来执行查询:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[private void listEvents(PrintWriter out, SimpleDateFormat dateFormatter) {
|
||
|
||
List result = HibernateUtil.getSessionFactory()
|
||
.getCurrentSession().createCriteria(Event.class).list();
|
||
if (result.size() > 0) {
|
||
out.println("<h2>Events in database:</h2>");
|
||
out.println("<table border='1'>");
|
||
out.println("<tr>");
|
||
out.println("<th>Event title</th>");
|
||
out.println("<th>Event date</th>");
|
||
out.println("</tr>");
|
||
for (Iterator it = result.iterator(); it.hasNext();) {
|
||
Event event = (Event) it.next();
|
||
out.println("<tr>");
|
||
out.println("<td>" + event.getTitle() + "</td>");
|
||
out.println("<td>" + dateFormatter.format(event.getDate()) + "</td>");
|
||
out.println("</tr>");
|
||
}
|
||
out.println("</table>");
|
||
}
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
最后,<literal>store</literal>动作会被导向到<literal>createAndStoreEvent()</literal>方法,它也使用当前线程的<literal>Session</literal>:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[protected void createAndStoreEvent(String title, Date theDate) {
|
||
Event theEvent = new Event();
|
||
theEvent.setTitle(title);
|
||
theEvent.setDate(theDate);
|
||
|
||
HibernateUtil.getSessionFactory()
|
||
.getCurrentSession().save(theEvent);
|
||
}]]></programlisting>
|
||
|
||
<para>
|
||
大功告成,这个servlet写完了。Hibernate会在单一的<literal>Session</literal> 和<literal>Transaction</literal>中处理到达的servlet请求。如同在前面的独立应用程序中那样,Hibernate可以自动的把这些对象绑定到当前运行的线程中。这给了你用任何你喜欢的方式来对代码分层及访问<literal>SessionFactory</literal>的自由。通常,你会用更加完备的设计,把数据访问代码转移到数据访问对象中(DAO模式)。请参见Hibernate Wiki,那里有更多的例子。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
<sect2 id="tutorial-webapp-deploy">
|
||
<title>部署与测试</title>
|
||
|
||
<para>
|
||
要发布这个程序,你得把它打成web发布包:WAR文件。把下面的脚本加入到你的<literal>build.xml</literal>中:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<target name="war" depends="compile">
|
||
<war destfile="hibernate-tutorial.war" webxml="web.xml">
|
||
<lib dir="${librarydir}">
|
||
<exclude name="jsdk*.jar"/>
|
||
</lib>
|
||
|
||
<classes dir="${targetdir}"/>
|
||
</war>
|
||
</target>]]></programlisting>
|
||
|
||
<para>
|
||
这段代码在你的开发目录中创建一个<literal>hibernate-tutorial.war</literal>的文件。它把所有的类库和<literal>web.xml</literal>描述文件都打包进去,web.xml 文件应该位于你的开发根目录中:
|
||
</para>
|
||
|
||
<programlisting><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
|
||
<web-app version="2.4"
|
||
xmlns="http://java.sun.com/xml/ns/j2ee"
|
||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
|
||
|
||
<servlet>
|
||
<servlet-name>Event Manager</servlet-name>
|
||
<servlet-class>events.EventManagerServlet</servlet-class>
|
||
</servlet>
|
||
|
||
<servlet-mapping>
|
||
<servlet-name>Event Manager</servlet-name>
|
||
<url-pattern>/eventmanager</url-pattern>
|
||
</servlet-mapping>
|
||
</web-app>]]></programlisting>
|
||
|
||
<para>
|
||
请注意在你编译和部署web应用程之前,需要一个附加的类库:<literal>jsdk.jar</literal>。这是Java Servlet开发包,假若你还没有,可以从Sun网站上下载,把它copy到你的lib目录。但是,它仅仅是在编译时需要,不会被打入WAR包。
|
||
</para>
|
||
|
||
<para>
|
||
在你的开发目录中,调用<literal>ant war</literal>来构建、打包,然后把<literal>hibernate-tutorial.war</literal>文件拷贝到你的tomcat的<literal>webapps</literal>目录下。假若你还没安装Tomcat,就去下载一个,按照指南来安装。对此应用的发布,你不需要修改任何Tomcat的配置。
|
||
</para>
|
||
|
||
<para>
|
||
在部署完,启动Tomcat之后,通过<literal>http://localhost:8080/hibernate-tutorial/eventmanager</literal>进行访问你的应用,在第一次servlet 请求发生时,请在Tomcat log中确认你看到Hibernate被初始化了(<literal>HibernateUtil</literal>的静态初始化器被调用),假若有任何异常抛出,也可以看到详细的输出。
|
||
</para>
|
||
|
||
</sect2>
|
||
|
||
</sect1>
|
||
<sect1 id="tutorial-summary">
|
||
<title>
|
||
总结
|
||
</title>
|
||
|
||
<para>
|
||
本章覆盖了如何编写一个简单独立的Hibernate命令行应用程序及小型的Hibernate web应用程序的基本要素。
|
||
</para>
|
||
|
||
<para>
|
||
如果你已经对Hibernate感到自信,通过开发指南目录,继续浏览你感兴趣的内容-那些会被问到的问题大多是事务处理 (<xref linkend="transactions"/>),抓取(fetch)的效率 (<xref linkend="performance"/>),或者API的使用 (<xref linkend="objectstate"/>)和查询的特性(<xref linkend="objectstate-querying"/>)。
|
||
</para>
|
||
|
||
<para>
|
||
别忘了去Hibernate的网站查看更多(有针对性的)示例。
|
||
</para>
|
||
|
||
</sect1>
|
||
</chapter>
|