Hibernate参考文档-第一章.Hibernate入门

href="http://www.hibernate.org/hib_docs/v3/reference/en/shared/css/html.css" type="text/css" rel="stylesheet" /> 

第一章.Hibernamte入门

1.1. 前言

这一章是给Hibernate新手的入门教程。我们从一个简单的使用内存数据库进行简单开发的命令行程序来理解它的步骤。

这个教程是针对Hibernate的新手的,但是需要Java和SQL的知识。它是根据 Michael Gloegl 的教程,我们需要的第三方库是JDK 1.4 和 5.0。你也可能会需要 JDK 1.3.

这个教程的源代码在发行包的doc/reference/tutorial/ 目录下。

1.2. 第一部分 - 第一个Hibernate应用

首先,我们将要创建一个基于控制台的简单Hibernate应用。我们使用Java数据库 (HSQL DB),所以我们不必安装任何数据库服务器。

假设我们需要一个小型的数据库应用系统记录我们想要参加的活动和这些活动的主人的信息。

我们要做的第一件事是搭建开发目录并把我们需要的所有的Java lib 包在里面。从Hibernate网站上下载Hibernate发行包。解压压缩包,把 /lib 下的所有jar包放到你开发目录的 /lib 目录下。应该像下面这样:

.
+lib
  antlr.jar
  cglib.jar
  asm.jar
  asm-attrs.jars
  commons-collections.jar
  commons-logging.jar
  hibernate3.jar
  jta.jar
  dom4j.jar
  log4j.jar 

这是Hibernate编写过程中所需lib的最小集合(注意,我们也把hibernate3.jar) 。你当前用的Hibernate版本可能需要更多或更少lib。查看Hibernate发行包的lib/目录下的 README.txt 文件来获得必需和可选的第三方lib的更多信息。(实际上,Log4j不是必需的,但是是很多开发者首选的。)

接下来我们创建一个类来描述我们想要存储在数据库里的活动。

1.2.1. 第一个类

我们的第一个持久化类是一个含有一些属性的简单的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和setter方法用了标准的JavaBean的命名习惯,属性域的访问级别是private。我们推荐这样的设计,但也不是必需的。Hibernate也可以直接访问域,用方法访问的好处是增强了重构性 。没有参数的构造方法是为了通过映射构造一个类的示例对象。

id属性记录一个特定活动的的唯一标识符的值。如果我们要充分利用Hibernate的功能,所有的持久化实体类(那些不太重要的依赖类也是)都需要这样一个标识符属性。事实上,大多数应用(例如,web应用)需要通过标识符区分对象,所以你应该认为这是一个功能而不是限制。然而,我们不必操作对象的标识符,因此,setter方法应该是私有的(private)。当一个对象要保存时只有Hibernate可以指派标识符。你能够看到,Hibernate可以访问public,private,protected属性的方法。这个选择取决于你,你可以选择来适合你的应用设计。

没有参数的构造方法是所有持久化类所与需要的;Hibernate通过Java映射(Java Reflection)为你创建对象。构造方法可以是私有的,但是所在的包对于运行时代理器和高效的数据检索必须是可见的。

把这个Java源文件放在开发文件夹的src目录下,目录结构如下:

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java

下一步,我们告诉Hibernate这个持久类。

1.2.2. 映射文件

Hibernate 需要知道怎样加载和存储持久类的对象。Hibernate映射文件正是在这里起作用。映射文件告诉Hibernate要访问数据库中的哪个表,使用表里的哪些字段。

映射文件的基本结构如下:

<?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映射元素和属性,你应该在你的文本编辑器中打开DTD文件,这是你总揽所有元素和属性,查看默认值和注释的最简便的方法。注意,Hibernate不从网络上加载DTD文件,而是首先从应用程序的classpath中查找。DTD文件在 hibernate3.jar和Hibernate发行包的 src/ 目录下都有。

为了简化代码在下面的例子里我们会忽略DTD定义,这当然不是随意的。

在两个hibernate-mapping 标签之间,包含了一个 class 元素。所有持久化的实体类 (也可能是在后面会出现的不是第一级实体的依赖类)需要这样映射到SQL数据库中的表:

<hibernate-mapping>

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

    </class>

</hibernate-mapping>

到此为止我们告诉了Hibernate怎样持久化和加载Event类的对象到表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使用getter和setter方法来访问这个属性。column属性告诉Hibernate我们用EVENTS表的那一个列作为这个主键。嵌套的generator 元素指定了标识符的生成策略,在这里我们使用 native,它将根据配置的数据库(方言)选择最佳的策略。 Hibernate支持数据库生成的全局唯一的标识符,也支持应用程序指定的(或者是你外加的任何策略)。

最后,我们在映射文件中加上这个类的持久化属性的定义。类的属性不会被默认为是持久化的:

<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 属性而 title 没有呢?如果没有 column 属性Hibernate会默认的将类的属性名作为数据库的列名。title在这里会运行得很好,然而,date在大多数数据库中是保留字所以我们最好把它映射成别的名字。

下一个有趣的事是 title 映射也缺少一个 type 属性。可能像你预期的一样,我们声明和使用的类型不是Java数据类型。也不是SQL数据库中的类型。这些类型被称为Hibernate映射类型(Hibernate mapping types) ,它能将Java数据类型转换成SQL数据类型,反之亦然。如果影射中没有type属性Hibrenate会自己确定正确的转换和映射类型。在一些情况下,这种自动检测(使用Java类的反射机制) 可能不是你期望或者需要的类型。date属性就是这种情况,Hibernate不知道应该映射成SQL的date,timestamp还是time类型的列。我们用timestamp转换类型映射date属性来保存完整的日期和时间信息。

这个映射文件应该保存为Event.hbm.xml,保存在Event类源文件所在的目录.映射文件的名字可以是任意的,然而hbm.xml后缀是Hibernate开发者使用的命名习惯。目录结构如下:

.
+lib
  <Hibernate and third-party libraries>
+src
  +events
    Event.java
    Event.hbm.xml

我们继续进行Hiberante的主要配置。

1.2.3. Hibernate 配置

我们现在已经有了持久化类和他的映射文件。现在该配置Hibernate了。在这之前,我们需要一个数据库。 HSQL DB,一个基于Java的SQL数据库管理系统,可以从HSQL DB 网站下载。实际上,你只需要其中的 hsqldb.jar 把这个文件放在开发文件夹的 lib/ 目录下。

在开发目录的根下创建一个名为data 的路径,HSQL DB的数据文件将放在这里。现在通过在这个数据目录下运行 java -classpath ../lib/hsqldb.jar org.hsqldb.Server 启动数据库。你可以看到它启动并且绑定到一个TCP/IP socket上,稍后我们的应用程序要连接到这里。如果你要在这个教程里启动一个新的数据库,关掉HSQL DB(在窗口中按CTRL+C),删除data目录下的所有数据文件,重启HSQL DB。

Hibernate在你的应用中是数据库连接层,所以它需要连接信息。连接通过我们将要配置的JDBC连接池完成。Hibernate发行包里包含了几个开源的JDBC连接池工具,但是在这个教程里我们使用Hibernate的built-in 连接池。如果你想用第三方的JDBC连接池软件注意把需要的库拷贝到你的classpah中并且使用不同的连接池设置。

在Hibernate的配置中,我们可以使用简单的hibernate.properties文件,一个稍微复杂点的hibernate.cfg.xml 文件,或者是完整的结构格局。大多数用户喜欢用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 - 一个负责特定数据库的全局工厂。如果你有多个数据库,使用多个 <session-factory> 配置,这通常放在多个配置文件中(为了方便启动)。

开始的四个 property 元素包含了JDBC连接的比要配置。property语句指名了Hibernate生成的特定的SQL变量。Hibernate的自动Session持久化上下文管理马上就会看到。hbm2ddl.auto 操作会直接在数据库中自动生成数据库计划。这当然可以关掉(通过去掉这个配置选项)或者通过ant任务的SchemaExport的帮助间接生成一个文件。最后我们将持久类的映射文件加到配置中。

把这个文件拷贝到源代码目录,它将会发布在classpath的根目录下。在启动的时候Hibernate自动在classpath的根目录下寻找命名为 hibernate.cfg.xml 的文件。

1.2.4. 使用Ant构建

现在我们要使用Ant来构建这个教程示例。你需要按装Ant - 从 Ant下载页得到ant。这里不会讲解怎样安装ant,请参看 Ant 手册。安装ant之后,我们可以开始创建build文件。它将命名成 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源文件以外的文件拷贝到target目录下,例如配置文件和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,这个启动包括创建一个全局的SessionFactory对象 We have to startup Hibernate并把它存在应用代码容易访问的地方。一个SessionFactory能够开启新的会话。一个Session是单线程的工作单元,SessionFactory 是线程安全的全局对象,只加载一次。

我们将创建一个HibernateUtil的帮助类来负责简化启动和访问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;
    }

}

这个类不仅在它的静态初始化(只在类加载的时候被Java虚拟机调用一次)产生一个全局化的SessionFactory,而且隐藏了它使用静态单线程的事实。它可能还在一个应用服务的JNDI中查找SessionFactory 。

如果你在配置文件中给SessionFactory命名,Hibernate会在它创建之后试图绑定在JNDI中,为了完全避免这段代码你应该使用JMX部署,让JMX-capable容器实例化HibernateService并绑定在JNDI中。 这些高级选项将在Hibernate参考文档中讨论。

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

这也将顺利编译。我们最后需要配置一个日志系统 - Hibernate使用commons logging ,你可以选择 Log4j和 JDK 1.4 logging。大多数开发者喜欢 Log4j:把log4j.properties 从Hibernate发布包 (在 etc/ 目录下)中拷贝到你的 src 目录下,跟 hibernate.cfg.xml并列。看一下配置示例,如果你想要更详细的输出更改一下设置。默认情况下,只有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并且在数据库中执行INSERTS语句。在运行之前让我们看一下 Session 和 Transaction-提交代码。

 Session 是一个单独的工作单元。现在我们使事情简单一写,假设Hibernate Session和属于据库事务之间是一对一的。我们使用Hibernate Session会用到的Transaction API来从底层的事务系统中(在这个例子中用JDBC,但是也可以通过JTA运行)保护我们的代码(不必修改)。

sessionFactory.getCurrentSession() 是做什么的?首先,一旦你得到SessionFactory(多亏HibernateUtil使它简化),你可以随时随地的调用它,getCurrentSession() 方法总是返回“当前”的工作单元。记得我们在hibernate.cfg.xml中把这个选项的配置更改成“thread”了吗?因此,当前的工作单元跟执行我们的应用的Java线程绑定在一起。然而,这不是全部内容,你还必须考虑它的作用范围,什么时候开始,什么时候结束。

当第一次调用getCurrentSession()的时候,一个 Session 在第一次需要的时候开始。然后被Hibernate绑定到当前的线程中。当通过事件提交或回滚结束一个事务时,Hibernate自动从线程中释放Session并为你关闭它。如果你再一次调用 getCurrentSession(),你将得到一个新的Session 并能够开始一个新的工作单元。这种线程绑定编程模型是Hibernate中最常见的方式, 它允许你的代码灵活层叠(事务划分代码和数据访问代码分离,我们将在后面这样做)。

根工作单元的范围有关,Hibernate Session应该用来执行一个数据库操作还是多个呢?上面的例子使用一个Session进行一个操作。这纯粹是偶然的,这个例子只是不够复杂而不能展示其他方法。Hibernate Session的范围是灵活的,但是你决不能使用通过一个新的Hibernate Session操作所有的数据库来设计你的应用程序。即使你在下面的例子里又看到这样几次,可以认为一个Seesion对应一个操作时反模型的(anti-pattern)。在这个教程的后面会有一个真正的(网络)应用。

查看 第十一章,事物和并发 获得更多的事务处理和划分的信息。在前面的例子中我们跳过了所有的错误处理和回滚。

为了运行这个程序,我们要在Ant build文件中增加可执行的任务:

<target name="run" depends="compile">
    <java fork="true" classname="events.EventManager" classpathref="libraries">
        <classpath path="${targetdir}"/>
        <arg value="${action}"/>
    </java>
</target>

action参数的值在调用target时在命令行中设定:

C:/hibernateTutorial/>ant run -Daction=store

在完成编译并且启动Hibernate之后,根据你的配置你可以看到大量的log输出。在最后你会看见下面这样一行:

[java] Hibernate: insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)

这表示Hibernate执行了INSERT语句,问号表示在JDBC中绑定的参数。要想查看绑定的参数或者减少冗长的log,请检查你的 log4j.properties 文件。

现在,我们还想列出存储的活动,所以我们在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)从数据库中查询加载所有存在Event对象。Hibernate会生成适当的SQL语句,传输到数据库然后将数据封装成Event对象。当然,你可以用HQL创建更复杂的查询。

按照下面的步骤运行测试一下:

  • 运行 ant 命令 run -Daction=store 在数据库中存储一些数据,当然在这之前会通过hbm2ddl生成数据库结构。
  • 现在从你的hibernate.cfg.xml文件中注释掉hbm2ddl功能。通常你在不断的单元测试中会开着它,但是运行一次hbm2ddl会丢弃你曾经存储的所有数据 - 在配置中设置为create 实际上可以翻译为“当SessionFactory创建时删掉库中的所有表然后重建”。

如果你现在加上 -Daction=list运行Ant,你应该会看到到目前为止你存储在数据库中的活动。你当然也可以运行几次store

注意:大多数Hibernate新手会在这里失败,问题通常都是 Table not found 错误信息。然而,如果你遵循了上面的要点就不会有这样的问题,因为hbm2ddl在第一次运行时创建数据库结构,后来重新运行的应用会用到这个数据库。如果你更改了影射和(或者)数据库结构,必须在一次使hbm2ddl生效。

1.3. 第二部分 - 联合映射

我们把一个持久化的实体类映射成了数据表。让我们在这基础上添加一些类的联系。首先,我们在应用中增加用户角色,并且存储一系列他们参加的活动。

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"/>

现在我们要在这两个实体之间创建应用。显然,人们可以参加活动,活动要有参加者。我们必须要解决的设计问题是:方向性、多元性和行为集合。

1.3.2. 单向关联

我们要在Person类中增加一个活动的集合。那样我们不必执行一个外部查询而可以通过调用aPerson.getEvents()方法很容易地遍历某一个人的活动。因为集合中不含有相同元素并且我们不关心顺序,我们使用Javade集合中的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()方法。从功能角度来看,这不是必需的,你也可以通过一个外查询来获得某一个特定活动的参与者,这是一个取决于你的设计。但是通过我们的讨论,关联的多元化是非常清楚地:两边都可以有多个值,我们把这称为多对多关联。因此,我们使用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实体关系)中,需要一个关联表。表中的每一行记录人和活动的连接。表的名字在set元素的table属性中配置。在<key>元素中定义关联中Pserson表的主键列,Event表的列名在<many-to-many>元素的column属性中定义。你还要告诉Hibernate你的集合中的对象的类(更正:是集合引用的另一端的类)。

因此,这个映射的数据库结构变成:

    _____________        __________________
   |             |      |                  |       _____________
   |   EVENTS    |      |   PERSON_EVENT   |      |             |
   |_____________|      |__________________|      |    PERSON   |
   |             |      |                  |      |_____________|
   | *EVENT_ID   | <--> | *EVENT_ID        |      |             |
   |  EVENT_DATE |      | *PERSON_ID       | <--> | *PERSON_ID  |
   |  TITLE      |      |__________________|      |  AGE        |
   |_____________|                                |  FIRSTNAME  |
                                                  |  LASTNAME   |
                                                  |_____________|
 

1.3.3. 运行关联

在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自动探测到集合被更改并需要更新。这被称为“自动脏检查”,你也可以通过修改你的任何一个对象的名称或日期属性来测试一下。只要他们是持久化状态,也就说这一个特定的Hibernate Session中(例如,他们刚刚在一个工作单元中被加载或者保存),Hibernate监控任何改动并执行隐含的SQL。将内存同步到数据库的进程成为flushing,它通常在工作单元的最后执行。在我们的代码中,工作单元通过提交(或回滚)数据库事务结束,这是在CurrentSessionContext类的线程配置操作中定义的。

你也可能在不同的工作单元中加载的person和event,或者你在Session之外修改了一个不是持久状态的对象(如果之前已经持久化了,我们把这种状态称为分离)。你甚至可以修改一个分离状态的集合:

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的主方法中并在命令行中调用它来完成这个联系。如果你需要需要一个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,我们把这些类称为值类型,他们的实例要依靠特定的实体。这些类型的实例既没有自己的标识符,也不会在实体之间共享(两个人即使有相同的姓也不会指向同一个firstname对象)。当然,值类型不仅能在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>

根前面的映射不同的是元素部分,它告诉Hibernate集合中不包含对其它实体的引用,而是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   |
                                                |_____________|
 

你可以看到,集合表的主键是两个列的组合键。这也意味着一个人不能有重复的email地址,这也正是Java中Set的语义。

你现在可以向我们之前连接persons和events那样试着往这个集合中添加元素。在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();
}

我们没有使用查询来初始化集合。因此调用getter方法是会触发一个额外的select语句来初始化,所以我们可以为它增加一个元素。监控SQL日志并试着优化它。

1.3.5. 双向关联

接下来我们进行双向关联映射 - 在Java里实现person和event进行互操作的映射。当然,不会改变数据库结构,我们仍然使用多对多关系。关系型数据库比网络编程语言灵活得多,它不需要遍历的方向-数据可以通过任何可能的方式查看和获取。

首先,增加一个参与者的集合到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>

可以看到,在两个映射文件中都有正常的集合映射。注意,在两个映着文档中key元素和many-to-many元素的列名是相反的。这里最重要的一点是Event集合映射中的set元素增加了inverse="true"属性。

这意味着当需要获得双方连接信息时Hibernate会从另一方-Person类中获得。当你要了解两个实体间的双向连接是怎样创建的时会容易理解得多。

1.3.6. 操作双向连接

首先,记住Hibernate不会影响到正常的Java语义。我们是怎样在单项连接中创建Person和Event的连接的呢?我们在一个Person实例中的event集合里添加了一个Event的实例。所以,显然如果我们要使这个连接成为双向的,就要在另一端做同样的事--在Event的Person集合中增加一个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 - 这允许同一个包中的类和子类还可以访问这些方法,但是保证其他类不会直接弄乱集合(大多数情况下会)。你应该在连接的另一端做相同的事。

inverse 映射属性是做什么的呢?对于你和Java语言来说,双向关联仅仅是在双方正确的设置引用。然而Hibernate没有足够的信息来正确安排SQL的INSERT和UPDATE 语句(来避免违反约束),并需要一些帮助来处理双向关联。使一方关联反转是告诉Hibernate忽略它,把它认为是另一方的镜像。这是Hibernate完成所有方向性遍历模型到SQL数据库结构转化的全部必需的。你一定要记住的规则是:所有的双向关联需要一方反转。在一对多关联中它应该是“多”的一段,在多对多关联中可以是任意一端,这没有区别。

让我们在一个小的web应用中来运行。

1.4. 第三部分 - EventManager web 应用

一个Hibernate网络应用使用Session和Transaction跟独立应用中几乎一样。不管怎样,一些共用的模式是有用的。我们现在来写一个EventManagerServlet。这个servlet能够列出存储在数据库中的所有的活动,并生成一个HTML表单来加入新的活动。

1.4.1. 编写基本的servlet

在你源文件目录下events包中创建一个新的类:

package events;

// Imports

public class EventManagerServlet extends HttpServlet {

    // Servlet code
}

这个servlet只处理HTTP GET请求,因此,我们实现doGet()方法:

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

}

我们在这里应用的模式是每一请求一个session。当一个请求提交到servlet时一个新的Hibernate session会通过sessionFactory的第一次调用getCurrentSession()开启。然后就开始了一个数据库事务-所有数据访问都在事务内发生,不管是读还是写数据(我们在应用中不使用自动提交方式)。

不要对每一个数据库操作都是用新的Hibernate Session。在一个请求范围内使用一个Hibernate Sessio,使用getCurrentSession()以便自动跟当前的Java线程绑定在一起。

接下来很快要处理请求的行为并表现成应答HTML。

最后,当处理和表现完成后工作单元就结束了。如果在处理和翻译过程中出现任何问题,将会抛出一个异常并且数据库事务回滚。这就完成了一个每一请求一个Session模式。你也可以写一个servlet过滤器来替换每一个servlet中的事务划分代码。查看Hibernate网站和Wiki获得这一模式的更多信息,这种模式称为在视图中打开Session(Open Session in View)。在你考虑用Jsp呈现你的视图而不是用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, dateFormatter);

// Write HTML footer
out.println("</body></html>");
out.flush();
out.close();

理所当然,将Java和HTML混合的代码风格不会在比较复杂的应用中大量出现- 记住我们只是在这个教程中举例说明Hibernate的基本概念。这段代码打印一个HTML的头和尾。在这个页面里,一个event实体的HTML表单和数据库中所有活动的列表会输出出来。第一段代码是微不足道的,仅输出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, 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>");
    }
}

最后,存储行为通过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完成了。一个servlet请求会在一个单独的Session和事务中处理。像早先单独应用中一样,Hibernate能够把这些对象自动绑定到执行的当前线程中。这给你提供了你使用任何方式来分离代码和SessionFactory的自由。通常,你应该使用更加成熟的设计将数据访问代码转移到数据访问对象(DAO模式)中。在Hibernate Wiki中查找更多的例子。

1.4.3. 发布和测试

为了发布这个应用程序,你要创建一个web文档,WAR文件。在bulid.xml中增加下面的ant任务:

<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打包:

<?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>

在你打包和发布之前,注意有一个额外的包是必须的jsdk.jar。这是Java servlet开发工具,如果你还没有这个库,从SUN的网站上下载并复制到你的lib目录下。然而,它只在编译的时候有用并且不会包含在WAR包中。

在你的项目目录下运行ant war创建并配置war包,然后把hibernate-tutorial.war文件拷贝到你的Tomcat webapp 目录下。如果你没有安装Tomcat,下载并遵循安装指导。你不必改变任何Tomcat配置来发布你的应用。

一旦发布并且Tomcat在运行,通过http://localhost:8080/hibernate-tutorial/eventmanager来访问应用。确保你监测Tomcat日志来查看Hibernate在第一次请求servlet时初始化(HibernateUtil的静态初始化被调用)并且如果有异常获得更多的详细信息。

1.5. 总结

这个教程讲述了编写一个简单的Hibernate独立应用程序和小型网络应用的基础。

如果你已经对Hibernate有了足够的信心,继续浏览参考文档的目录来查找你感兴趣的话题 - 大多数的问题集中在事务处理(第11章,事务和并行), 查询性能(第19章,性能提升),和API的使用(第10章,使用对象) 以及查询特点 (10.4节,“查询”).

不要忘了在Hibernate网站上获得更多的(专门)教程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值