Hibernate参考文档

HIBERNATE - 符合Java习惯的关系数据库持久化
Hibernate参考文档
3.1.2
________________________________________
第 1 章  Hibernate入门
1.1.  前言
本章是面向Hibernate初学者的一个入门教程。我们从一个使用驻留内存式(in-memory)数据库的简单命令行应用程序开始, 用易于理解的方式逐步开发。
本章面向Hibernate初学者,但需要Java和SQL知识。它是在Michael Goegl所写的指南的基础上完成的。在这里,我们称第三方库文件是指JDK 1.4和5.0。若使用JDK1.3,你可能需要其它的库文件。
本章的源代码已包含在发布包中,位于doc/reference/tutorial/目录下。
1.2.  第一部分 - 第一个Hibernate应用程序
首先我们将创建一个简单的基于控制台的(console-based)Hibernate应用程序。由于我们使用Java数据库(HSQL DB),所以不必安装任何数据库服务器。
假设我们希望有一个小应用程序可以保存我们希望参加的活动(events)和这些活动主办方的相关信息。 (译者注:在本教程的后面部分,我们将直接使用event而不是它的中文翻译“活动”,以免混淆。)
我们所做的第一件事就是创建我们的开发目录,并且把所有需要用到的Java库文件放进去。解压缩从Hibernate网站下载的Hibernate发布包,并把/lib目录下所有需要的库文件拷到我们新建开发目录下的/lib目录下。看起来就像这样:
.
+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
到编写本文时为止,这些是Hibernate运行所需要的最小库文件集合(注意我们也拷贝了 Hibernate3.jar,这个是最主要的文件)。你正使用的Hibernate版本可能需要比这更多或少一些的库文件。请参见发布包中的lib/目录下的README.txt,以获取更多关于所需和可选的第三方库文件信息(事实上,Log4j并不是必须的库文件,但被许多开发者所喜欢)。
接下来我们创建一个类,用来代表那些我们希望储存在数据库里的event。
1.2.1.  第一个class
我们的第一个持久化类是一个带有一些属性(property)的简单JavaBean类:
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;
    }
}
你可以看到这个类对属性的存取方法(getter and setter method)使用了标准JavaBean命名约定,同时把类属性(field)的访问级别设成私有的(private)。这是推荐的设计,但并不是必须的。Hibernate也可以直接访问这些field,而使用访问方法(accessor method)的好处是提供了重构时的健壮性(robustness)。为了通过反射机制(Reflection)来实例化这个类的对象,我们需要提供一个无参的构造器(no-argument constructor)。
对一特定的event, id 属性持有唯一的标识符(identifier)的值。如果我们希望使用Hibernate提供的所有特性,那么所有的持久化实体(persistent entity)类(这里也包括一些次要依赖类)都需要一个这样的标识符属性。而事实上,大多数应用程序(特别是web应用程序)都需要通过标识符来区别对象,所以你应该考虑使用标识符属性而不是把它当作一种限制。然而,我们通常不会操作对象的标识(identity),因此它的setter方法的访问级别应该声明private。这样当对象被保存的时候,只有Hibernate可以为它分配标识符值。你可看到Hibernate可以直接访问public,private和protected的访问方法和field。所以选择哪种方式完全取决于你,你可以使你的选择与你的应用程序设计相吻合。
所有的持久化类(persistent classes)都要求有无参的构造器,因为Hibernate必须使用Java反射机制来为你创建对象。构造器(constructor)的访问级别可以是private,然而当生成运行时代理(runtime proxy)的时候则要求使用至少是package 级别的访问控制,这样在没有字节码指令(bytecode instrumentation)的情况下,从持久化类里获取数据会更有效率。
把这个Java源代码文件放到开发目录下的src目录里,注意包位置要正确。 现在这个目录看起来应该像这样:
.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java
下一步,我们把这个持久化类的信息告诉Hibernate。
1.2.2.  映射文件
Hibernate需要知道怎样去加载(load)和存储(store)持久化类的对象。这正是Hibernate映射文件发挥作用的地方。映射文件告诉Hibernate它,应该访问数据库(database)里面的哪个表(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里使用它来自动完成那些用来映射的XML元素(element)和属性(attribute)。你也可以在文本编辑器里打开DTD-这是最简单的方式来概览所有的元素和attribute,并查看它们的缺省值以及注释。注意Hibernate不会从web加载DTD文件,但它会首先在应用程序的classpath中查找。DTD文件已包括在hibernate3.jar里,同时也在Hibernate发布包的src/目录下。
为缩短代码长度,在以后的例子里我们会省略DTD的声明。当然,在实际的应用程序中,DTD声明是必须的。
在hibernate-mapping标签(tag)之间, 含有一个class元素。所有的持久化实体类(再次声明,或许接下来会有依赖类,就是那些次要的实体)都需要一个这样的映射,来把类对象映射到SQL数据库里的表。
<hibernate-mapping>

    <class name="events.Event" table="EVENTS">

    </class>

</hibernate-mapping>
到目前为止,我们告诉了Hibernate怎样把Events类的对象持久化到数据库的EVENTS表里,以及怎样从EVENTS表加载到Events类的对象。每个实例对应着数据库表中的一行。现在我们将继续讨论有关唯一标识符属性到数据库表的映射。另外,由于我们不关心怎样处理这个标识符,我们就配置由Hibernate的标识符生成策略来产生代理主键字段。
<hibernate-mapping>

    <class name="events.Event" table="EVENTS">
        <id name="id" column="EVENT_ID">
            <generator class="native"/>
        </id>
    </class>

</hibernate-mapping>
id元素是标识符属性的声明,name="id" 声明了Java属性的名字 - Hibernate会使用getId()和setId()来访问它。 column属性则告诉Hibernate, 我们使用EVENTS表的哪个字段作为主键。嵌套的generator元素指定了标识符生成策略,在这里我们指定native,它根据已配置的数据库(方言)自动选择最佳的标识符生成策略。Hibernate支持由数据库生成,全局唯一性(globally unique)和应用程序指定(或者你自己为任何已有策略所写的扩展)这些策略来生成标识符。
最后我们在映射文件里面包含需要持久化属性的声明。默认情况下,类里面的属性都被视为非持久化的:
<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>
和id元素一样,property元素的name属性告诉Hibernate使用哪个getter和setter方法。在此例中,Hibernate会寻找getDate()/setDate(), 以及getTitle()/setTitle()。
为什么date属性的映射含有column attribute,而title却没有?当没有设定column attribute 的时候,Hibernate缺省地使用JavaBean的属性名作为字段名。对于title,这样工作得很好。然而,date在多数的数据库里,是一个保留关键字,所以我们最好把它映射成一个不同的名字。
另一有趣的事情是title属性缺少一个type attribute。我们在映射文件里声明并使用的类型,却不是我们期望的那样,是Java数据类型,同时也不是SQL数据库的数据类型。这些类型就是所谓的Hibernate 映射类型(mapping types),它们能把Java数据类型转换到SQL数据类型,反之亦然。再次重申,如果在映射文件中没有设置type属性的话,Hibernate会自己试着去确定正确的转换类型和它的映射类型。在某些情况下这个自动检测机制(在Java 类上使用反射机制)不会产生你所期待或需要的缺省值。date属性就是个很好的例子,Hibernate无法知道这个属性(java.util.Date类型的)应该被映射成:SQL date,或timestamp,还是time 字段。在此例中,把这个属性映射成timestamp 转换器,这样我们预留了日期和时间的全部信息。
应该把这个映射文件保存为Event.hbm.xml,且就在EventJava类的源文件目录下。映射文件可随意地命名,但hbm.xml的后缀已成为Hibernate开发者社区的约定。现在目录结构看起来应该像这样:
.
+lib
  <Hibernate and third-party libraries>
+src
 +events
  Event.java
  Event.hbm.xml
我们继续进行Hibernate的主要配置。
1.2.3.  Hibernate配置
现在我们已经有了一个持久化类和它的映射文件,该是配置Hibernate的时候了。在此之前,我们需要一个数据库。 HSQL DB是种基于Java 的SQL数据库管理系统(DBMS),可以从HSQL DB的网站上下载。实际上,你只需下载的包中的hsqldb.jar文件,并把这个文件放在开发文件夹的lib/目录下即可。
在开发的根目录下创建一个data目录 - 这是HSQL DB存储数据文件的地方。此时在data目录中运行java -classpath lib/hsqldb.jar org.hsqldb.Server就可启动数据库。你可以在log中看到它的启动,及绑定到TCP/IP套结字,这正是我们的应用程序稍后会连接的地方。如果你希望在本例中运行一个全新的数据库,就在窗口中按下CTRL + C来关闭HSQL数据库,并删除data/目录下的所有文件,再重新启动HSQL数据库。
Hibernate是你的应用程序里连接数据库的那层,所以它需要连接用的信息。连接(connection)是通过一个也由我们配置的JDBC连接池(connection pool)来完成的。Hibernate的发布包里包含了许多开源的(open source)连接池,但在我们例子中使用Hibernate内置的连接池。注意,如果你希望使用一个产品级(production-quality)的第三方连接池软件,你必须拷贝所需的库文件到你的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: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>
注意这个XML配置使用了一个不同的DTD。在这里,我们配置了Hibernate的SessionFactory-一个关联于特定数据库全局的工厂(factory)。如果你要使用多个数据库,就要用多个的<session-factory>,通常把它们放在多个配置文件中(为了更容易启动)。
最开始的4个property元素包含必要的JDBC连接信息。方言(dialect)的property元素指明Hibernate 生成的特定SQL变量。你很快会看到,Hibernate对持久化上下文的自动session管理就会派上用场。 打开hbm2ddl.auto选项将自动生成数据库模式(schema)- 直接加入数据库中。当然这个选项也可以被关闭(通过去除这个配置选项)或者通过Ant任务SchemaExport的帮助来把数据库schema重定向到文件中。最后,在配置中为持久化类加入映射文件。
把这个文件拷贝到源代码目录下面,这样它就位于classpath的根目录的最后。Hibernate在启动时会自动在classpath的根目录查找名为hibernate.cfg.xml的配置文件。
1.2.4.  用Ant构建
现在我们用Ant来构建应用程序。你必须先安装Ant-可以从Ant 下载页面得到它。怎样安装Ant就不在这里介绍了,请参考Ant 用户手册。当你安装完了Ant,就可以开始创建build.xml文件,把它直接放在开发目录下面。
一个简单的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
1.2.5.  启动和辅助类
是时候来加载和储存一些Event对象了,但首先我们得编写一些基础的代码以完成设置。我们必须启动Hibernate,此过程包括创建一个全局的SessoinFactory,并把它储存在应用程序代码容易访问的地方。SessionFactory可以创建并打开新的Session。一个Session代表一个单线程的单元操作,SessionFactory则是个线程安全的全局对象,只需要被实例化一次。
我们将创建一个HibernateUtil辅助类(helper class)来负责启动Hibernate和更方便地操作SessionFactory。让我们来看一下它的实现:
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;
    }

}
这个类不但在它的静态初始化过程(仅当加载这个类的时候被JVM执行一次)中产生全局的SessionFactory,而且隐藏了它使用了静态singleton的事实。它也可能在应用程序服务器中的JNDI查找SessionFactory。
如果你在配置文件中给SessionFactory一个名字,在SessionFactory创建后,Hibernate会试着把它绑定到JNDI。要完全避免这样的代码,你也可以使用JMX部署,让具有JMX能力的容器来实例化HibernateService并把它绑定到JNDI。这些高级可选项在后面的章节中会讨论到。
把HibernateUtil.java放在开发目录的源代码路径下,与放events的包并列:
.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java
    Event.hbm.xml
  +util
    HibernateUtil.java
  hibernate.cfg.xml
+data
build.xml
再次编译这个应用程序应该不会有问题。最后我们需要配置一个日志(logging)系统 - Hibernate使用通用日志接口,允许你在Log4j和JDK 1.4 日志之间进行选择。多数开发者更喜欢Log4j:从Hibernate的发布包中(它在etc/目录下)拷贝log4j.properties到你的src目录,与hibernate.cfg.xml.放在一起。看一下配置示例,如果你希望看到更加详细的输出信息,你可以修改配置。默认情况下,只有Hibernate的启动信息才会显示在标准输出上。
示例的基本框架完成了 - 现在我们可以用Hibernate来做些真正的工作。
1.2.6.  加载并存储对象
我们终于可以使用Hibernate来加载和存储对象了,编写一个带有main()方法的EventManager类:
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();
    }

}
我们创建了个新的Event对象并把它传递给Hibernate。现在Hibernate负责与SQL打交道,并把INSERT命令传给数据库。在运行之前,让我们看一下处理Session和Transaction的代码。
一个Session就是个单一的工作单元。我们暂时让事情简单一些,并假设HibernateSession和数据库事务是一一对应的。为了让我们的代码从底层的事务系统中脱离出来(此例中是JDBC,但也可能是JTA),我们使用Hibernate Session中的Transaction API。
sessionFactory.getCurrentSession()是干什么的呢?首先,只要你持有SessionFactory(幸亏我们有HibernateUtil,可以随时获得),大可在任何时候、任何地点调用这个方法。getCurrentSession()方法总会返回“当前的”工作单元。记得我们在hibernate.cfg.xml中把这一配置选项调整为"thread"了吗?因此,当前工作单元的范围就是当前执行我们应用程序的Java线程。但是,这并非总是正确的。 Session在第一次被使用的时候,或者第一次调用getCurrentSession()的时候,其生命周期就开始。然后它被Hibernate绑定到当前线程。当事务结束的时候,不管是提交还是回滚,Hibernate也会把Session从当前线程剥离,并且关闭它。假若你再次调用getCurrentSession(),你会得到一个新的Session,并且开始一个新的工作单元。这种线程绑定(thread-bound)的编程模型(model)是使用Hibernate的最广泛的方式。
关于事务处理及事务边界界定的详细信息,请参看第 11 章 事务和并发。在上面的例子中,我们也忽略了所有的错误与回滚的处理。
为第一次运行我们的程序,我们得在Ant的build文件中增加一个可以调用得到的target。
<target name="run" depends="compile">
    <java fork="true" classname="events.EventManager" classpathref="libraries">
        <classpath path="${targetdir}"/>
        <arg value="${action}"/>
    </java>
</target>
action参数(argument)的值是通过命令行调用这个target的时候设置的:
C:\hibernateTutorial\>ant run -Daction=store
你应该会看到,编译以后,Hibernate根据你的配置启动,并产生一大堆的输出日志。在日志最后你会看到下面这行:
[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)
这是Hibernate执行的INSERT命令,问号代表JDBC的绑定参数。如果想要看到绑定参数的值或者减少日志的长度,就要调整你在log4j.properties文件里的设置。
我们想要列出所有已经被存储的events,就要增加一个条件分支选项到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.getSessionFactory().getCurrentSession();

    session.beginTransaction();

    List result = session.createQuery("from Event").list();

    session.getTransaction().commit();

    return result;
}
我们在这里是用一个HQL(Hibernate Query Language-Hibernate查询语言)查询语句来从数据库中加载所有存在的Event对象。Hibernate会生成适当的SQL,把它发送到数据库,并操作从查询得到数据的Event对象。当然,你可以使用HQL来创建更加复杂的查询。
现在,根据以下步骤来执行并测试以上各项:
• 运行ant run -Daction=store来保存一些内容到数据库。当然,先得用hbm2ddl来生成数据库schema。
• 现在把hibernate.cfg.xml文件中hbm2ddl属性注释掉,这样我们就取消了在启动时用hbm2ddl来生成数据库schema。通常只有在不断重复进行单元测试的时候才需要打开它,但再次运行hbm2ddl会把你保存的一切都删掉(drop)——create配置的真实含义是:“在创建SessionFactory的时候,从schema 中drop 掉所有的表,再重新创建它们”。
如果你现在使用命令行参数-Daction=list运行Ant,你会看到那些至今为止我们所储存的events。当然,你也可以多调用几次store以保存更多的envents。
注意,很多Hibernate新手在这一步会失败,我们不时看到关于Table not found错误信息的提问。但是,只要你根据上面描述的步骤来执行,就不会有这个问题,因为hbm2ddl会在第一次运行的时候创建数据库schema,后继的应用程序重起后还能继续使用这个schema。假若你修改了映射,或者修改了数据库schema,你必须把hbm2ddl重新打开一次。
1.3.  第二部分 - 关联映射
我们已经映射了一个持久化实体类到表上。让我们在这个基础上增加一些类之间的关联。首先我们往应用程序里增加人(people)的概念,并存储他们所参与的一个Event列表。(译者注:与Event一样,我们在后面将直接使用person来表示“人”而不是它的中文翻译)
1.3.1.  映射Person类
最初简单的Person类:
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'

}
创建一个名为Person.hbm.xml的新映射文件(别忘了最上面的DTD引用):
<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>
最后,把新的映射加入到Hibernate的配置中:
<mapping resource="events/Event.hbm.xml"/>
<mapping resource="events/Person.hbm.xml"/>
现在我们在这两个实体之间创建一个关联。显然,persons可以参与一系列events,而events也有不同的参加者(persons)。我们需要处理的设计问题是关联方向(directionality),阶数(multiplicity)和集合(collection)的行为。
1.3.2.  单向Set-based的关联
我们将向Person类增加一连串的events。那样,通过调用aPerson.getEvents(),就可以轻松地导航到特定person所参与的events,而不用去执行一个显式的查询。我们使用Java的集合类(collection):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()。从功能的角度来说,这并不是必须的。因为你总可以显式地执行一个查询,以获得某个特定event的所有参与者。这是个在设计时需要做出的选择,完全由你来决定,但此讨论中关于关联的阶数是清楚的:即两端都是“多”值的,我们把它叫做多对多(many-to-many)关联。因而,我们使用Hibernate的多对多映射:
<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>
Hibernate支持各种各样的集合映射,<set>使用的最为普遍。对于多对多关联(或叫n:m实体关系), 需要一个关联表(association table)。表里面的每一行代表从person到event的一个关联。表名是由set元素的table属性配置的。关联里面的标识符字段名,对于person的一端,是由<key>元素定义,而event一端的字段名是由<many-to-many>元素的column属性定义。你也必须告诉Hibernate集合中对象的类(也就是位于这个集合所代表的关联另外一端的类)。
因而这个映射的数据库schema是:
    _____________        __________________
   |             |      |                  |       _____________
   |   EVENTS    |      |   PERSON_EVENT   |      |             |
   |_____________|      |__________________|      |    PERSON   |
   |             |      |                  |      |_____________|
   | *EVENT_ID   | <--> | *EVENT_ID        |      |             |
   |  EVENT_DATE |      | *PERSON_ID       | <--> | *PERSON_ID  |
   |  TITLE      |      |__________________|      |  AGE        |
   |_____________|                                |  FIRSTNAME  |
                                                  |  LASTNAME   |
                                                  |_____________|
 
1.3.3.  使关联工作
我们把一些people和events 一起放到EventManager的新方法中:
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();
}
在加载一Person和Event后,使用普通的集合方法就可容易地修改我们定义的集合。如你所见,没有显式的update()或save(),Hibernate会自动检测到集合已经被修改并需要更新回数据库。这叫做自动脏检查(automatic dirty checking),你也可以尝试修改任何对象的name或者date属性,只要他们处于持久化状态,也就是被绑定到某个Hibernate 的Session上(如:他们刚刚在一个单元操作被加载或者保存),Hibernate监视任何改变并在后台隐式写的方式执行SQL。同步内存状态和数据库的过程,通常只在单元操作结束的时候发生,称此过程为清理缓存(flushing)。在我们的代码中,工作单元由数据库事务的提交(或者回滚)来结束——这是由CurrentSessionContext类的thread配置选项定义的。
当然,你也可以在不同的单元操作里面加载person和event。或在Session以外修改不是处在持久化(persistent)状态下的对象(如果该对象以前曾经被持久化,那么我们称这个状态为脱管(detached))。你甚至可以在一个集合被脱管时修改它:
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();
}
对update的调用使一个脱管对象重新持久化,你可以说它被绑定到一个新的单元操作上,所以在脱管状态下对它所做的任何修改都会被保存到数据库里。这也包括你对这个实体对象的集合所作的任何改动(增加/删除)。
这对我们当前的情形不是很有用,但它是非常重要的概念,你可以把它融入到你自己的应用程序设计中。在EventManager的main方法中添加一个新的动作,并从命令行运行它来完成我们所做的练习。如果你需要person及event的标识符 — 那就用save()方法返回它(你可能需要修改前面的一些方法来返回那个标识符):
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);
上面是个关于两个同等重要的实体类间关联的例子。像前面所提到的那样,在特定的模型中也存在其它的类和类型,这些类和类型通常是“次要的”。你已看到过其中的一些,像int或String。我们称这些类为值类型(value type),它们的实例依赖(depend)在某个特定的实体上。这些类型的实例没有它们自己的标识(identity),也不能在实体间被共享(比如,两个person不能引用同一个firstname对象,即使他们有相同的first name)。当然,值类型并不仅仅在JDK中存在(事实上,在一个Hibernate应用程序中,所有的JDK类都被视为值类型),而且你也可以编写你自己的依赖类,例如Address,MonetaryAmount。
你也可以设计一个值类型的集合,这在概念上与引用其它实体的集合有很大的不同,但是在Java里面看起来几乎是一样的。
1.3.4.  值类型的集合
我们把一个值类型对象的集合加入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的集合(在映射中使用小写的名字”string“是向你表明它是一个Hibernate的映射类型或者类型转换器)。和之前一样,set元素的table属性决定了用于集合的表名。key元素定义了在集合表中外键的字段名。element元素的column属性定义用于实际保存String值的字段名。
看一下修改后的数据库schema。
  _____________        __________________
 |             |      |                  |       _____________
 |   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   |
                                                |_____________|
 
你可以看到集合表的主键实际上是个复合主键,同时使用了2个字段。这也暗示了对于同一个person不能有重复的email地址,这正是Java里面使用Set时候所需要的语义(Set里元素不能重复)。
你现在可以试着把元素加入到这个集合,就像我们在之前关联person和event的那样。其实现的Java代码是相同的:
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();
}
这次我们没有使用fetch查询来初始化集合。因此,调用其getter方法会触发另一附加的select来初始化集合,这样我们才能把元素添加进去。检查SQL log,试着通过预先抓取来优化它。
1.3.5.  双向关联
接下来我们将映射双向关联(bi-directional association)- 在Java里让person和event可以从关联的任何一端访问另一端。当然,数据库schema没有改变,我们仍然需要多对多的阶数。一个关系型数据库要比网络编程语言 更加灵活,所以它并不需要任何像导航方向(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="events.Person"/>
</set>
如你所见,两个映射文件里都有普通的set映射。注意在两个映射文件中,互换了key和many-to-many的字段名。这里最重要的是Event映射文件里增加了set元素的inverse="true"属性。
这意味着在需要的时候,Hibernate能在关联的另一端 - Person类得到两个实体间关联的信息。这将会极大地帮助你理解双向关联是如何在两个实体间被创建的。
1.3.6.  使双向连起来
首先请记住,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)中的类以及继承自这个类的子类可以访问这些方法,但禁止其他任何人的直接访问,避免了集合内容的混乱。你应尽可能地在另一端也把集合的访问级别设成protected。
inverse映射属性究竟表示什么呢?对于你和Java来说,一个双向关联仅仅是在两端简单地正确设置引用。然而,Hibernate并没有足够的信息去正确地执行INSERT和UPDATE语句(以避免违反数据库约束),所以它需要一些帮助来正确的处理双向关联。把关联的一端设置为inverse将告诉Hibernate忽略关联的这一端,把这端看成是另外一端的一个镜象(mirror)。这就是所需的全部信息,Hibernate利用这些信息来处理把一个有向导航模型转移到数据库schema时的所有问题。你只需要记住这个直观的规则:所有的双向关联需要有一端被设置为inverse。在一对多关联中它必须是代表多(many)的那端。而在多对多(many-to-many)关联中,你可以任意选取一端,因为两端之间并没有差别。
让我们把进入一个小型的web应用程序。
1.4. 第三部分 - EventManager web应用程序
Hibernate web应用程序使用Session 和Transaction的方式几乎和独立应用程序是一样的。但是,有一些常见的模式(pattern)非常有用。现在我们编写一个EventManagerServlet。这个servlet可以列出数据库中保存的所有的events,还提供一个HTML表单来增加新的events。
1.4.1. 编写基本的servlet
在你的源代码目录的events包中创建一个新的类:
package events;

// Imports

public class EventManagerServlet extends HttpServlet {

    private final SimpleDateFormat dateFormatter =
                            new SimpleDateFormat("dd.MM.yyyy");

    // Servlet code
}
我们后面会用到dateFormatter 的工具, 它把Date对象转换为字符串。只要一个formatter作为servlet的成员就可以了。
这个servlet只处理 HTTP GET 请求,因此,我们要实现的是doGet()方法:
protected void doGet(HttpServletRequest request,
                     HttpServletResponse response)
        throws ServletException, IOException {

    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);
    }

}
我们称这里应用的模式为每次请求一个session(session-per-request)。当有请求到达这个servlet的时候,通过对SessionFactory的第一次调用,打开一个新的Hibernate Session。然后启动一个数据库事务—所有的数据访问都是在事务中进行,不管是读还是写(我们在应用程序中不使用auto-commit模式)。
下一步,对请求的可能动作进行处理,渲染出反馈的HTML。我们很快就会涉及到那部分。
最后,当处理与渲染都结束的时候,这个工作单元就结束了。假若在处理或渲染的时候有任何错误发生,会抛出一个异常,回滚数据库事务。这样,session-per-request模式就完成了。为了避免在每个servlet中都编写事务边界界定的代码,可以考虑写一个servlet 过滤器(filter)来更好地解决。关于这一模式的更多信息,请参阅Hibernate网站和Wiki,这一模式叫做Open Session in View—只要你考虑用JSP来渲染你的视图(view),而不是在servlet中,你就会很快用到它。
1.4.2. 处理与渲染
我们来实现处理请求以及渲染页面的工作。
// 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);

// Write HTML footer
out.println("</body></html>");
out.flush();
out.close();
必须承认,这种编码风格让Java与HTML混合在了一起,在更复杂的应用程序中不应大量地使用—记住我们在章中仅为了展示Hibernate的基本概念。这段代码打印出了HTML头和尾部。在页面中,打印出一个输入event条目的表单,并列出数据库中所有events。第一个方法微不足道,仅仅是输出HTML:
private void printEventForm(PrintWriter out) {
    out.println("<h2>Add new event:</h2>");
    out.println("<form>");
    out.println("Title: <input name='eventTitle' length='50'/><br/>");
    out.println("Date (e.g. 24.12.2009): <input name='eventDate' length='10'/><br/>");
    out.println("<input type='submit' name='action' value='store'/>");
    out.println("</form>");
}
listEvents()方法使用绑定到当前线程的Hibernate Session来执行查询:
private void listEvents(PrintWriter out) {
    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>");
    }
}
最后,store动作会被导向到createAndStoreEvent()方法,它也使用当前线程的Session:
protected void createAndStoreEvent(String title, Date theDate) {
    Event theEvent = new Event();
    theEvent.setTitle(title);
    theEvent.setDate(theDate);

    HibernateUtil.getSessionFactory()
                    .getCurrentSession().save(theEvent);
}
大功告成,这个servlet写完了。Hibernate会在单一的Session 和Transaction中处理到达的servlet请求。如同在前面的独立应用程序中那样,Hibernate可以自动的把这些对象绑定到当前运行的线程中。这给了你用任何你喜欢的方式来对代码分层及访问SessionFactory的自由。通常,你会用更加完备的设计,把数据访问代码转移到数据访问对象中(DAO模式)。请参见Hibernate Wiki,那里有更多的例子。
1.4.3. 部署与测试
要发布这个程序,你得把它打成web发布包:WAR文件。把下面的脚本加入到你的build.xml中:
<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>
这段代码在你的开发目录中创建一个hibernate-tutorial.war的文件。它把所有的类库和web.xml描述文件都打包进去,web.xml 文件应该位于你的开发根目录中:
<?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>
请注意在你编译和部署web

你可能感兴趣的:(sql,Hibernate,xml,servlet,ant)