1.1. 第一部分 - 第一个 Hibernate 应用程序

优质
小牛编辑
145浏览
2023-12-01

在这个例子里,我们将设立一个小应用程序可以保存我们希望参加的活动(events)和这些活动主办方的相关信息。(译者注:在本教程的后面部分,我们将直接使用 event 而不是它的中文翻译“活动”,以免混淆。)

注意

虽然你可以使用任何数据库,我们还是用 HSQLDB(一个用 Java 编写的内存数据库)来避免花费篇章对数据库服务器的安装/配置进行解释。

1.1.1. 设置

我们需要做的第一件事情是设置开发环境。我们将使用许多构建工具如 Maven 所鼓吹的“标准格式”。特别是 Maven,它的资源对这个格式(layout)有着很好的描述。因为本教程使用的是 web 应用程序,我么将创建和使用 src/main/javasrc/main/resourcessrc/main/webapp 目录。

在本教程里我们将使用 Maven,利用其 transitive dependency 管理以及根据 Maven 描述符用 IDE 自动设置项目的能力。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion
>4.0.0</modelVersion>

    <groupId
>org.hibernate.tutorials</groupId>
    <artifactId
>hibernate-tutorial</artifactId>
    <version
>1.0.0-SNAPSHOT</version>
    <name
>First Hibernate Tutorial</name>

    <build>
         <!-- we dont want the version to be part of the generated war file name -->
         <finalName
>${artifactId}</finalName>
    </build>

    <dependencies>
        <dependency>
            <groupId
>org.hibernate</groupId>
            <artifactId
>hibernate-core</artifactId>
        </dependency>

        <!-- Because this is a web app, we also have a dependency on the servlet api. -->
        <dependency>
            <groupId
>javax.servlet</groupId>
            <artifactId
>servlet-api</artifactId>
        </dependency>

        <!-- Hibernate uses slf4j for logging, for our purposes here use the simple backend -->
        <dependency>
            <groupId
>org.slf4j</groupId>
            <artifactId
>slf4j-simple</artifactId>
        </dependency>

        <!-- Hibernate gives you a choice of bytecode providers between cglib and javassist -->
        <dependency>
            <groupId
>javassist</groupId>
            <artifactId
>javassist</artifactId>
        </dependency>
    </dependencies>

</project
>

提示

It is not a requirement to use Maven. If you wish to use something else to build this tutorial (such as Ant), the layout will remain the same. The only change is that you will need to manually account for all the needed dependencies. If you use something like Ivy providing transitive dependency management you would still use the dependencies mentioned below. Otherwise, you'd need to grab all dependencies, both explicit and transitive, and add them to the project's classpath. If working from the Hibernate distribution bundle, this would mean hibernate3.jar, all artifacts in the lib/required directory and all files from either the lib/bytecode/cglib or lib/bytecode/javassist directory; additionally you will need both the servlet-api jar and one of the slf4j logging backends.

把这个文件保存为项目根目录下的 pom.xml

1.1.2. 第一个 class

接下来我们创建一个类,用来代表那些我们希望储存在数据库里的 event,这是一个具有一些属性的简单 JavaBean 类:

package org.hibernate.tutorial.domain;

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)。

对一特定的 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)的情况下,从持久化类里获取数据会更有效率。

把这个文件保存到 src/main/java/org/hibernate/tutorial/domain 目录下。

1.1.3. 映射文件

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 package="org.hibernate.tutorial.domain">
[...]
</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 package="org.hibernate.tutorial.domain">

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

    </class>

</hibernate-mapping
>

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

<hibernate-mapping package="org.hibernate.tutorial.domain">

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

</hibernate-mapping
>

id 元素是对 identifier 属性的声明。name="id" 映射属性声明了 JavaBean 属性的名称并告诉 Hibernate 使用 getId()setId() 方法来访问这个属性。column 属性告诉 Hibernate EVENTS 表的哪个字段持有主键值。

嵌套的 generator 元素指定标识符的生成策略(也就是标识符值是怎么产生的)。在这个例子里,我们选择 native,它提供了取决于数据库方言的可移植性。Hibernate 数据库生成的、全局性唯一的以及应用程序分配的标识符。标识符值的生成也是 Hibernate 的扩展功能之一,你可以插入自己的策略。

提示

native is no longer consider the best strategy in terms of portability. for further discussion, see 第 26.4 节 “标识符的生成”

最后我们在映射文件里面包含需要持久化属性的声明。默认情况下,类里面的属性都被视为非持久化的:


<hibernate-mapping package="org.hibernate.tutorial.domain">

    <class name="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 转换器,这样我们预留了日期和时间的全部信息。

提示

当处理映射文件时,Hibernate 用反射(reflection)来决定这个映射类型。这需要时间和资源,所以如果你注重启动性能,你应该考虑显性地定义所用的类型。

把这个映射文件保存为 src/main/resources/org/hibernate/tutorial/domain/Event.hbm.xml

1.1.4. Hibernate 配置

此时,你应该有了持久化类和它的映射文件。现在是配置 Hibernate 的时候了。首先让我们设立 HSQLDB 使其运行在“服务器模式”。

注意

数据在程序运行期间需要保持有效。

在开发的根目录下创建一个 data 目录 - 这是 HSQL DB 存储数据文件的地方。此时在 data 目录中运行 java -classpath ../lib/hsqldb.jar org.hsqldb.Server 就可启动数据库。你可以在 log 中看到它的启动,及绑定到 TCP/IP 套接字,这正是我们的应用程序稍后会连接的地方。如果你希望在本例中运行一个全新的数据库,就在窗口中按下 CTRL + C 来关闭 HSQL 数据库,并删除 data/ 目录下的所有文件,再重新启动 HSQL 数据库。

Hibernate 将为你的应用程序连接到数据库,所以它需要知道如何获取连接。在这个教程里,我们使用一个独立连接池(和 javax.sql.DataSource 相反)。Hibernate 支持两个第三方的开源 JDBC 连接池:c3p0 和 proxool。然而,在本教程里我们将使用 Hibernate 内置的连接池。

小心

嵌入的 Hibernate 连接池不用于产品环境。它缺乏连接池里的几个功能。

为了保存 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"
>update</property>

        <mapping resource="org/hibernate/tutorial/domain/Event.hbm.xml"/>

    </session-factory>

</hibernate-configuration
>

注意

请注意,这个配置文件指定了一个不同的 DTD。

注意这个 XML 配置使用了一个不同的 DTD。在这里,我们配置了 Hibernate 的SessionFactory — 一个关联于特定数据库全局的工厂(factory)。如果你要使用多个数据库,就要用多个的 <session-factory>,通常把它们放在多个配置文件中(为了更容易启动)。

签名 4 个 property 元素包含了 JDBC 连接所必需的配置。方言 property 元素指定了 Hibernate 生成的特定 SQL 语句。

提示

In most cases, Hibernate is able to properly determine which dialect to use. See 第 26.3 节 “方言的使用” for more information.

最开始的 4 个 property 元素包含必要的 JDBC 连接信息。方言(dialect)的 property 元素指明 Hibernate 生成的特定 SQL 变量。你很快会看到,Hibernate 对持久化上下文的自动 session 管理就会派上用场。 打开 hbm2ddl.auto 选项将自动生成数据库模式(schema)- 直接加入数据库中。当然这个选项也可以被关闭(通过去除这个配置选项)或者通过 Ant 任务 SchemaExport 的帮助来把数据库 schema 重定向到文件中。最后,在配置中为持久化类加入映射文件。

把这个文件保存为 src/main/resources 目录下的 hibernate.cfg.xml

1.1.5. 用 Maven 构建

我们将用 Maven 构建这个教程。你将需要安装 Maven;你可以从 Maven 下载页面获得 Maven。Maen 将读取我们先前创建的 /pom.xml 并知道执行基本的项目任务。首先,让我们运行 compile 目标来确保我们可以编译到目前为止的所有程序:

[hibernateTutorial]$ mvn compile
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building First Hibernate Tutorial
[INFO]    task-segment: [compile]
[INFO] ------------------------------------------------------------------------
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Compiling 1 source file to /home/steve/projects/sandbox/hibernateTutorial/target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] Finished at: Tue Jun 09 12:25:25 CDT 2009
[INFO] Final Memory: 5M/547M
[INFO] ------------------------------------------------------------------------

1.1.6. 启动和辅助类

是时候来加载和储存一些 Event 对象了,但首先我们得编写一些基础的代码以完成设置。我们必须启动 Hibernate,此过程包括创建一个全局的 SessoinFactory,并把它储存在应用程序代码容易访问的地方。SessionFactory 可以创建并打开新的 Session。一个 Session 代表一个单线程的单元操作,org.hibernate.SessionFactory 则是个线程安全的全局对象,只需要被实例化一次。

我们将创建一个 HibernateUtil 辅助类(helper class)来负责启动 Hibernate 和更方便地操作 org.hibernate.SessionFactory。让我们来看一下它的实现:

package org.hibernate.tutorial.util;

import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {

    private static final SessionFactory sessionFactory = buildSessionFactory();

    private static SessionFactory buildSessionFactory() {
        try {
            // Create the SessionFactory from hibernate.cfg.xml
            return 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;
    }

}

把这段代码保存为 src/main/java/org/hibernate/tutorial/util/HibernateUtil.java

这个类不但在它的静态初始化过程(仅当加载这个类的时候被 JVM 执行一次)中产生全局的 org.hibernate.SessionFactory,而且隐藏了它使用了静态 singleton 的事实。它也可能在应用程序服务器中的 JNDI 查找 org.hibernate.SessionFactory

如果你在配置文件中给 org.hibernate.SessionFactory 一个名字,在 它创建后,Hibernate 会试着把它绑定到 JNDI。要完全避免这样的代码,你也可以使用 JMX 部署,让具有 JMX 能力的容器来实例化 HibernateService 并把它绑定到 JNDI。这些高级可选项在后面的章节中会讨论到。

再 次编译这个应用程序应该不会有问题。最后我们需要配置一个日志(logging)系统 — Hibernate 使用通用日志接口,允许你在 Log4j 和 JDK 1.4 日志之间进行选择。多数开发者更喜欢 Log4j:从 Hibernate 的发布包中(它在 etc/ 目录下)拷贝 log4j.properties 到你的 src 目录,与 hibernate.cfg.xml 放在一起。看一下配置示例,如果你希望看到更加详细的输出信息,你可以修改配置。默认情况下,只有 Hibernate 的启动信息才会显示在标准输出上。

示例的基本框架完成了 — 现在我们可以用 Hibernate 来做些真正的工作。

1.1.7. 加载并存储对象

We are now ready to start doing some real work with Hibernate. Let's start by writing an EventManager class with a main() method:

package org.hibernate.tutorial;

import org.hibernate.Session;

import java.util.*;

import org.hibernate.tutorial.domain.Event;
import org.hibernate.tutorial.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();
    }

}

createAndStoreEvent() 来里我们创建了一个新的 Event 对象并把它传递给 Hibernate。现在 Hibernate 负责与 SQL 打交道,并把 INSERT 命令传给数据库。

A org.hibernate.Session is designed to represent a single unit of work (a single atomic piece of work to be performed). For now we will keep things simple and assume a one-to-one granularity between a Hibernate org.hibernate.Session and a database transaction. To shield our code from the actual underlying transaction system we use the Hibernate org.hibernate.Transaction API. In this particular case we are using JDBC-based transactional semantics, but it could also run with JTA.

sessionFactory.getCurrentSession() 是干什么的呢?首先,只要你持有 org.hibernate.SessionFactory,大可在任何时候、任何地点调用这个方法。getCurrentSession() 方法总会返回“当前的”工作单元。记得我们在 src/main/resources/hibernate.cfg.xml 中把这一配置选项调整为 "thread" 了吗?因此,因此,当前工作单元被绑定到当前执行我们应用程序的 Java 线程。但是,这并非是完全准确的,你还得考虑工作单元的生命周期范围(scope),它何时开始,又何时结束。

重要

Hibernate 提供三种跟踪当前会话的方法。基于“线程”的方法不适合于产品环境,它仅用于 prototyping 和教学用途。后面将更详细地讨论会话跟踪。

org.hibernate.Session 在第一次被使用的时候,即第一次调用 getCurrentSession() 的时候,其生命周期就开始。然后它被 Hibernate 绑定到当前线程。当事务结束的时候,不管是提交还是回滚,Hibernate 会自动把 org.hibernate.Session 从当前线程剥离,并且关闭它。假若你再次调用 getCurrentSession(),你会得到一个新的 org.hibernate.Session,并且开始一个新的工作单元。

和工作单元的生命周期这个话题相关,Hibernate org.hibernate.Session 是否被应该用来执行多次数据库操作?上面的例子对每一次操作使用了一个 org.hibernate.Session,这完全是巧合,这个例子不是很复杂,无法展示其他方式。Hibernate org.hibernate.Session 的生命周期可以很灵活,但是你绝不要把你的应用程序设计成为每一次数据库操作都用一个新的 Hibernate org.hibernate.Session。因此就算下面的例子(它们都很简单)中你可以看到这种用法,记住每次操作一个 session 是一个反模式。在本教程的后面会展示一个真正的(web)程序。

See 第 12 章 事务和并发 for more information about transaction handling and demarcation. The previous example also skipped any error handling and rollback.

要运行它,我们将使用 Maven exec 插件以及必要的 classpath 设置来进行调用:mvn exec:java -Dexec.mainClass="org.hibernate.tutorial.EventManager" -Dexec.args="store"

注意

你可能需要先执行 mvn compile

你应该会看到,编译以后,Hibernate 根据你的配置启动,并产生一大堆的输出日志。在日志最后你会看到下面这行:

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

执行 HQL INSERT 语句的例子如下:

我们想要列出所有已经被存储的 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;
    }

Here, we are using a Hibernate Query Language (HQL) query to load all existing Event objects from the database. Hibernate will generate the appropriate SQL, send it to the database and populate Event objects with the data. You can create more complex queries with HQL. See 第 15 章 HQL: Hibernate 查询语言 for more information.

现在我们可以再次用 Maven exec plugin - mvn exec:java -Dexec.mainClass="org.hibernate.tutorial.EventManager" -Dexec.args="list" 调用新的功能了。