第1章 Hibernateの導入

1.1. 前書き

この章はHibernateを初めて使うユーザ向けの入門的なチュートリアルです。 インメモリデータベースを使う簡単なコマンドラインアプリケーションから始め、 一歩一歩わかりやすいやり方で開発を進めます。

このチュートリアルはHibernateを初めて使うユーザを想定していますが、 理解するにはJavaとSQLについての知識が必要です。 これはMichael Gloeglの手によるチュートリアルを下敷きにしていますが、 ここでサードパーティライブラリと言っているのは、JDK 1.4と5.0用のものです。 JDK 1.3を利用するのであれば他のライブラリが必要かもしれません。

チュートリアルのソースコードはHibernateディストリビューションの doc/reference/tutorial/ にあります。

1.2. パート1 - 初めてのHibernateアプリケーション

最初にコンソールベースの簡単なHibernateアプリケーションを作成します。 Javaデータベース(HSQL DB)を利用するので、 データベースサーバをインストールする必要はありません。

仮に小さなデータベースアプリケーションが必要だとしましょう。 そのアプリケーションには出席したいイベントと、 そのイベントのホストについての情報を格納するものとします。

まず最初にすることは開発用のディレクトリをセットアップして、 必要となるすべてのJavaライブラリを配置することです。 HibernateウェブサイトからHibernateディストリビューションをダウンロードしてください。 ファイルを解凍して /lib にある必要なライブラリのすべてを、 新しい開発用ディレクトリの /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の必要最低限のライブラリです (メインアーカイブのhibernate3.jarもコピーしていることに注意してください)。 Hibernateのバージョンによってはさらに必要なライブラリや、不要なライブラリがあるかもしれません。 Hibernateディストリビューションの lib/ ディレクトリにある README.txt ファイルを見てください。 必須またはオプションのサードパーティライブラリについての情報を載せています (実際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;
    }
}

ご覧のとおり、このクラスはフィールドがprivateの可視性を持っているのと同時に、 JavaBean標準のゲッター、セッターメソッドの命名規約に従っています。 このような設計は推奨されていますが必須ではありません。 アクセサメソッドを設けるのはリファクタリングを考えた頑健性のためで、 Hibernateはフィールドに直接アクセスすることも可能です。 引数のないコンストラクタは、リフレクションでこのクラスのインスタンスを作成するために 必要です。

id プロパティは、ある特定のイベントに対するユニークな識別子の値を保持します。 Hibernateの完全な機能を使いたければ、すべての永続エンティティクラス (それほど重要ではない依存クラスというものもあります) にこのような識別子プロパティが必要になります。 事実上ほとんどのアプリケーション(特にwebアプリケーション)では、 識別子でオブジェクトを区別する必要があるため、これは制限というよりも特徴であると考えるべきです。 しかし通常オブジェクトのIDを操作するようなことはしません。 そのためセッターメソッドはprivateにするべきです。 Hibernateだけがオブジェクトがセーブされたときに識別子へ値を代入します。 Hibernateが(public, private, protected)フィールドに直接アクセスできるのと同様に、 public, private, protectedのアクセサメソッドにアクセスできるということがわかるでしょう。 選択はあなたに任されているので、あなたのアプリケーションの設計に合わせることができます。

引数のないコンストラクタはすべての永続クラスに必須です。 これはHibernateがJavaのリフレクションを使って、オブジェクトを作成しなければならないためです。 コンストラクタをprivateにすることは可能ですが、 実行時のプロキシ生成と、バイトコード操作なしの効率的なデータの抽出には、 package可視性が必要です。

開発フォルダの src というディレクトリの適切なパッケージに、 このJavaソースファイルを配置してください。 この時点でディレクトリは以下のようになっているはずです:

.
+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が非常に洗練されていることに注目してください。 このDTDは、エディタやIDEでのXMLマッピング要素と属性のオートコンプリーション機能に利用できます。 またDTDファイルをテキストエディタで開けてみてください。 というのも、すべての要素と属性を概観し、 コメントやデフォルトの値を見るには一番簡単な方法だからです。 Hibernateは、webからDTDファイルをロードせずに、 まずアプリケーションのクラスパスからこれを探し出そうとすることに注意してください。 DTDファイルはHibernateディストリビューションの src/ ディレクトリと同様、hibernate3.jar にも含まれています。

以降の例ではコードを短くするためにDTD宣言を省略します。 当然ですがこれはオプションではありません。

2つの hibernate-mapping タグの間に class 要素を含めてください。 すべての永続エンティティクラス(念を押しますが、 ファーストクラスのエンティティではない依存クラスというものが後ほど登場します) はSQLデータベース内のテーブルへのこのようなマッピングを必要とします。

<hibernate-mapping>

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

    </class>

</hibernate-mapping>

これまで私たちは、Event クラスのオブジェクトを EVENTS テーブルに対して、どのように永続化したりロードしたりするのかを Hibernateに教えてきました。そして個々のインスタンスはテーブルの行として表現されます。 それでは引き続きテーブルの主キーに対するユニークな識別子プロパティをマッピングしていきます。 さらに、この識別子の扱いに気を使いたくなかったのと同様に、 代理の主キーカラムに対する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はこのプロパティへアクセスするためにゲッター、セッターメソッドを使います。 カラム属性では EVENTS テーブルのどのカラムを主キーとして使うのかを Hibernateに教えます。 入れ子になっている generator 要素は、識別子を生成する時の戦略を指定します。 (この例では native を用いました)。 この要素は、設定したデータベース(dialect)に対する最良な識別子生成戦略を選定するものです。 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に教えます。 この例では、Hibernateは getDate()/setDate()getTitle()/setTitle() を 探します。

なぜ date プロパティのマッピングには column 属性があり、title プロパティにはないのでしょうか? column 属性がなければ、Hibernateはプロパティ名をカラム名として使います。 これは title では上手く行きます。 しかし date はほとんどのデータベースで予約語なので、 違う名前でマッピングした方がよいのです。

次に興味深いのは title マッピングが type 属性をも欠いている点です。 マッピングファイルで宣言して使うtypeは、おわかりかもしれませんがJavaのデータ型ではありません。 SQLデータベースの型でもありません。 これは Hibernateマッピング型 と呼ばれる、 JavaからSQLデータの型へまたはSQLからJavaデータ型へ翻訳するコンバータです。 繰り返しになりますが、Hibernateは type 属性がマッピングファイル内になければ、 正しいコンバージョンとマッピング型を自分で解決しようとします。 (Javaクラスのリフレクションを使った)この自動検知は、 場合によってはあなたが期待または必要とするデフォルト値にならないかもしれません。 date プロパティの場合がそうでした。 Hibernateはこの( java.util.Date の)プロパティを SQLの date , timestamp , time のうち、どのカラムにマッピングするべきなのかわかりません。 timestamp コンバータでプロパティをマッピングすることにより、完全な日時を保存します。

このマッピングファイルは、Event.hbm.xml として Event Javaクラスソースファイルのすぐ隣にセーブするべきです。 マッピングファイルの命名方法は任意ですが、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はここにデータファイルを格納します。 このデータディレクトリにおいて java -classpath lib/hsqldb.jar org.hsqldb.Server を実行し、 データベースを起動させてください。 動作の開始と、TCP/IPソケットのバインドが確認できます。 後ほど作成するアプリケーションはここに接続します。 もしこのチュートリアル中にデータベースを初期化したければ、 HSQL DBをシャットダウンして(作業ウィンドウで CTRL + C を押します) data/ ディレクトリ内のファイルを全て消去した後、 HSQL DBを再起動します。

Hibernateはアプリケーションのデータベースに接続する層なので、 コネクションの情報が必要になります。 コネクションはJDBCコネクションプールを通じて行われますが、これも設定する必要があります。 HibernateディストリビューションにはいくつかのオープンソースのJDBCコネクションプールツールが含まれていますが、 このチュートリアルではHibernateに組み込まれたコネクションプールを使います。 もし製品レベルの品質のサードパーティJDBCコネクションプールソフトウェアを使いたければ、 クラスパスに必要なライブラリをコピーして、異なるコネクションプールを設定しなければ ならないことに注意してください。

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> を使う設定にしてください。

最初の4つの property 要素はJDBCコネクションに必要な設定を含んでいます。 dialectという名前の property 要素は、Hibernateが生成する特定のSQL方言を指定します。 永続的なコンテキストに対するHibernateのセッションの自動管理は、後の例ですぐにわかるように、役に立つことでしょう。 hbm2ddl.auto オプションはデータベーススキーマの自動生成をonにします。 これは直接データベースに対して生成されます。 当然(configオプションを削除して)offにしたり、 SchemaExport というAntタスクの助けを借りてファイルにリダイレクトしたりできます。 最後に永続クラスのためのマッピングファイルを設定に追加します。

このファイルをソースディレクトリにコピーしてください。 するとこれはクラスパスのルートにあることになります。 Hibernateは、スタートアップ時にクラスパスのルートで hibernate.cfg.xml というファイルを自動的に探します。

1.2.4. Antによるビルド

それではAntを使ってチュートリアルをビルドしましょう。 それにはAntがインストールされていなければなりません。 Antダウンロードページ からダウンロードしてください。 Antのインストール方法はここでは説明しませんので、 Antマニュアル を参照してください。 Antをインストールすれば、ビルドファイルの作成を開始できます。 このファイルは build.xml と呼ばれ、開発ディレクトリに直接配置します。

基本的なビルドファイルはこのようになります:

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

これは .jar で終わるlibディレクトリのすべてのファイルを、 コンパイルに使用するクラスパスに追加することをAntに教えます。 また、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をスタートアップしなければなりません。 このスタートアップには、グローバルの SessionFactory オブジェクトを生成して、 それをアプリケーションのコードでアクセスしやすい場所に格納することが含まれます。 SessionFactory は新しく Session をオープンすることができます。 Session はシングルスレッドの作業単位(Unit of Work)を表現します。 それに対しSessionFactory はスレッドセーフのグローバルオブジェクトであり、 一度だけインスタンス化されます。

ここでスタートアップを行い、 便利に SessionFactory へアクセスする HibernateUtil ヘルパクラスを作成します。 実装を見てみましょう:

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 を生成するだけではなく、 静的シングルトンの使用を隠蔽します。 アプリケーションサーバーのJNDIから SessionFactory を ルックアップするのと同様です。

設定ファイル内で SessionFactory に名前を与えると、 Hibernateは SessionFactory 構築後にJNDIに対しバインドを行おうとします。 このコードを完全に排除するためには、 JMXデプロイメントを利用して JMXを利用できるコンテナをインスタンス化し、 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です。 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 を実行します。 実行前に SessionTransaction を処理するコードを確認してください。

Session は1つの作業単位(Unit of Work)です。 当分の間、話を簡単にするために、 SessionTransaction の対応を一対一と仮定します。 使用しているトランザクション・システム(このケースではJTAと共存できる単純なJDBC)からコードを保護するために、 Hibernate Session 上で利用可能な Transaction APIを使用します。

sessionFactory.getCurrentSession() はなにをするのでしょうか? まず、いったん SessionFactory を取得し保持すれば ( HibernateUtil のおかげで簡単です)、このメソッドを何度でも、 どこからでも呼び出すことが出来ます。 getCurrentSession() メソッドは常に「現在の」作業単位(Unit of Work)を返します。 hibernate.cfg.xml のこの機能の設定で、"thread"を指定したことを思い出してください。 このため現在の作業単位のスコープは、このアプリケーションを実行する現在のJavaスレッドです。 しかしこれで全てではありません。 Session は最初に必要となったとき、つまり最初に getCurrentSession() が 呼ばれたときに開始します。 そのときHibernateにより現在のスレッドに結び付けられます。 トランザクションが終了(コミットもしくはロールバック)したとき、 Hibernateもスレッドから Session を切り離し、クローズします。 再び getCurrentSession() を呼ぶと、 新しい Session を取得して新しい作業単位をスタートできます。 この thread-bound プログラミング・モデルはHibernateを利用する上で最も人気があります。

トランザクションの扱いと境界の詳しい情報については、 章 11. トランザクションと並行性 を見てください。 この例ではエラー処理やロールバックも割愛します。

この最初のルーチンを実行するには、Antのビルドファイルに呼び出し可能なターゲットを 追加しなければなりません:

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

action 引数の値は、ターゲットを呼ぶときにコマンドラインで設定します:

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

コンパイルすると、Hibernateがスタートし、あなたの設定によりますが、 多くのログ出力があるはずです。 その最後には以下の行があるでしょう:

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

これはHibernateが実行する INSERT で、 クエスチョンマークはJDBCバインドパラメータを表しています。 引数としてバインドされる値を見るため、あるいはログの冗長性を減らすためには、 log4j.properties をチェックしてください。

それでは同じように格納されたイベントの一覧を見ようと思います。 そのためメインメソッドにオプションを追加します:

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

ここですることは、データベースから存在するすべての Event オブジェクトをロードするHQL (Hibernate Query Language) クエリを使うことです。 Hibernateは適切なSQLを生成し、それをデータベースに送り、 そのデータを使って Event オブジェクトを生成します。 当然HQLでさらに複雑なクエリを作成できます。

以下のステップで、すべての実行とテストを行います。

  • hbm2ddlを通す前にデータベースのデータを作成し、データベーススキーマを生成するために、 ant run -Daction=store を実行してください。

  • 今は hibernate.cfg.xml ファイルのプロパティをコメントアウトしてhbm2ddlを無効にしてください。 通常は継続的に単体テストをしている間はhbm2ddlを有効にしておくのですが、 それ以外の場合にhbm2ddlを起動すると格納しておいた全てのデータを ドロップ するでしょう。 設定を create にすると、結果として 「SessionFactory生成の際、スキーマから全てのテーブルをドロップして再作成する」という設定になります。

-Daction=list と指定してAntを呼ぶと、 これまで格納したイベントが見えるはずです。 store アクションを数回以上呼ぶことも可能です。

注意:初めてHibernateに触れる人々の多くがここで失敗するため、Table not found エラーメッセージに 関する質問を定期的に見かけます。 しかし上記のステップに従えば、hbm2ddlが最初に実行されたときにデータベーススキーマを作成し、 その後の実行においてもこのスキーマを使用するので、問題は起こらないでしょう。 マッピングやデータベーススキーマを変更したときは、もう一度hbm2ddlを有効にしてください。

1.3. パート2 - 関連のマッピング

永続エンティティクラスをテーブルにマッピングしました。 さらにこの上にいくつかのクラスの関連を追加しましょう。 まず初めにアプリケーションに人々を追加し、彼らが参加するイベントのリストを格納します。

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

それではこれら2つのエンティティ間の関連を作成します。 人々がイベントに参加でき、イベントが参加者を持つのは明らかです。 扱わなければならない設計の問題は、方向、多重度、コレクションの振る舞いです。

1.3.2. 単方向Setベース関連

イベントのコレクションを Person クラスに追加します。 こうしておくと、明示的なクエリ、つまりわざわざaPerson.getEvents()を呼び出さずに、 特定の人に紐付くイベントを簡単にナビゲートすることができます。 私たちはJavaのコレクション、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のmany-to-manyマッピングを使います:

<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> 要素で、 イベント側のカラム名は <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();
}

PersonEvent をロードした後、 普通のコレクションメソッドを使って単純にそのコレクションを修正してください。 ご覧のとおり update()save() の明示的な呼び出しはありません。 Hibernateは、修正されたことにより更新する必要のあるコレクションを自動的に検知します。 これは 自動ダーティチェック と呼ばれ、 オブジェクトの名前やdateプロパティを修正することで試すことも可能です。 それらが 永続 状態にある限り、 つまり特定のHibernate Session にバインドされている限り (例えば作業単位(Unit of Work)の中で単にロードまたはセーブされた)、 Hibernateはどんな変更もモニターし、遅延書き込み(write-behind)でSQLを実行します。 通常、作業単位(Unit of Work)の最後にだけ行われるデータベースとメモリの状態を同期させる処理は、 フラッシュ と呼ばれます。 このコードでは、作業単位(Unit of Work)はデータベーストランザクションのコミット(もしくはロールバック)で終了します。 これは、 CurrentSessionContext クラスに対して thread を設定したためです。

異なる作業単位(Unit of Work)で人々とイベントをロードすることも当然できます。 そうでなければ、永続状態にないとき(以前に永続であったなら、この状態を 分離(detached) と呼びます)、 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 の呼び出しは分離オブジェクトを再び永続化します。 これは、新しい作業単位(Unit of Work)にバインドすると言えるでしょう。 そのため分離の間に加えられたどのような修正もデータベースにセーブできます。 エンティティオブジェクトのコレクションへの修正(追加・削除)も同様にセーブできます。

これは今はあまり使いみちがありませんが、 自分のアプリケーションの設計に組み込むことができる重要なコンセプトです。 それではこのエクササイズの最後に、 EventManager のメインメソッドに新しいアクションを追加して コマンドラインから呼び出してみましょう。 人やイベントの識別子が必要なら、 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);
}

これは同じように重要な2つのクラス、つまり2つのエンティティ間の関連の例でした。 前に述べたように、典型的なモデルには、普通「比較的重要ではない」他のクラスと型があります。 これまでに見たような intString のようなものです。 このようなクラスを 値型 と言います。 このインスタンスは特定のエンティティに 依存 します。 この型のインスタンスは独自のIDを持ちませんし、 エンティティ間で共有されることもありません (ファーストネームが同じだったとしても、2人の人は同じ firstname オブジェクトを参照しません)。 値型はもちろんJDK内に見つかりますが、それだけではなく (実際、HibernateアプリケーションにおいてすべてのJDKクラスは値型と見なせます)、 例えば AddressMonetaryAmount のような独自の依存クラスを書くこともできます。

値型のコレクションを設計することもできます。 これは他のエンティティへの参照のコレクションとは概念的に非常に異なりますが、 Javaではほとんど同じように見えます。

1.3.4. 値のコレクション

値型オブジェクトのコレクションを Person エンティティへ追加します。 Eメールアドレスを格納したいのですが、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 の部分ですが、 Hibernateにこのコレクションが他のエンティティへの参照を含まず、 String 型の要素のコレクションを含むことを教えます。 (小文字の名前(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   |
                                                |_____________|
 

コレクションテーブルの主キーは、実際は両方のカラムを使った複合キーであることがわかります。 これは人ごとにEメールアドレスが重複できないということで、 Javaのsetに要求されるセマンティクスそのものです。

以前人とイベントを関連づけたときと全く同じように、 今や試しにコレクションに要素を追加することができるようになりました。 両方とも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のログを監視して、即時フェッチを使って最適化してください。

1.3.5. 双方向関連

次に双方向関連をマッピングします。 Javaで両側から人とイベントの関連を動作させます。 もちろん、データベーススキーマは変わりませんが、多重度は多対多のままです。 リレーショナルデータベースはネットワークプログラミング言語よりも柔軟なので、 ナビゲーションの方向のようなものを必要としません。 データはあらゆるの方法で見たり復元できるということです。

まず 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>

ご覧のとおり、いずれのマッピングドキュメント(XMLファイル)でも、普通の set マッピングを使っています。 keymany-to-many のカラム名が、 両方のマッピングドキュメントで入れ替えになっていることに注目してください。 ここで最も重要な追加項目は、 Event のコレクションマッピングの set 要素にある inverse="true" 属性です。

この指定の意味は、2つの間のエンティティ間のリンクについての情報を探す必要があるとき、 Hibernateは反対側のエンティティ、つまり Person クラスから探すということです。 一度2つのエンティティ間の双方向リンクがどのように作成されるかがわかれば、 これを理解することはとても簡単です。

1.3.6. 双方向リンクの動作

まず、Hibernateが通常のJavaのセマンティクスに影響を及ぼさないことを心に留めておいてください。 私たちは、単方向の例としてどのように PersonEvent の間のリンクを作成したでしょうか? Person のインスタンスのイベントへの参照のコレクションに Event のインスタンスを追加しました。 そのためこのリンクを双方向にしたければ、 当たり前ですが反対側にも同じことをしなければなりません。 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);
}

コレクションのゲットとセットメソッドが現在protectedになっていることに注意してください。 これは同じパッケージのクラスやサブクラスのメソッドは依然アクセスが可能ですが、 (ほとんど)そのパッケージ外のどのクラスでも直接そのコレクションを台無しにすることを防ぎます。 おそらく反対側のコレクションにも同じことをした方がいいでしょう。

inverse マッピング属性とはいったい何でしょうか? 開発者とJavaにとっては、双方向リンクは単に両側の参照を正しく設定するということです。 しかしHibernateは(制約違反を避けるために)SQLの INSERTUPDATE 文を正確に変更するための十分な情報を持っていないので、 双方向関連プロパティを扱うための何らかの助けを必要とします。 関連の片側を inverse に設定することで、Hibernateは基本的には設定した側を無視し、 反対側の として考えます。 これだけで、Hibernateは方向を持つナビゲーションモデルをSQLデータベーススキーマへ変換するときの すべての問題にうまく対処できます。 覚えておかなければならないルールは簡単です。 双方向関連は必ず片側を inverse にする必要があるということです。 一対多関連ではそれは多側でなければなりません。 多対多関連ではどちら側でも構いません。どちらでも違いはありません。

ではこれを小さなWebアプリケーションにしてみましょう。

1.4. パート3 - EventManager Webアプリケーション

HibernateのWebアプリケーションは、スタンドアローンのアプリケーションのように SessionTransaction を使用します。 しかしいくつかの一般的なパターンが役立ちます。 ここで EventManagerServlet を作成します。このサーブレットは、 データベースに格納した全てのイベントをリストにでき、さらに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-per-request というパターンです。 Servletがリクエストを受け取ると、 SessionFactorygetCurrentSession() の最初の呼び出しで、 Hibernateの新しい Session が開かれます。 そのときデータベーストランザクションが開始されます。 データの読み書きに関わらず、すべてのデータアクセスはトランザクション内で行います。 (アプリケーション内ではオートコミットモードを使用しません)。

次に、リクエストのアクションは処理され、レスポンスであるHTMLが描画されます。 これについてはすぐに説明します。

最後にリクエストの処理とHTML描画が完了したときに、作業単位(Unit of Work)を終了します。 もし処理や描画中に問題が発生した場合、exceptionが投げられてデータベーストランザクションをロールバックします。 これで session-per-request パターンが完了します。 全てのサーブレットにトランザクション境界のコードを書く代わりに、サーブレットフィルタに記述することも可能です。 Open Session in View と呼ばれるこのパターンについては、 HibernateのWebサイトやWikiを参照してください。 サーブレットではなくJSPでHTML描画をしようとすると、すぐにこのパターンについての情報が必要になるでしょう。

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のヘッダーとフッターの記述です。 このページには、イベントを入力する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>");
    }
}

最後に、 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);
}

これでサーブレットの完成です。 サーブレットへのリクエストは、一つの SessionTransaction で処理されるでしょう。 最初のスタンドアローンのアプリケーションのように、 Hibernateは自動的にこれらのオブジェクトを実行するスレッドに結び付けることができます。 これにより、開発者が自由にコードをレイヤー分けでき、 好きな方法で SessionFactory へのアクセスができるようになります。 通常、開発者はより洗練されたデザインを使用して、データアクセスのコードを データアクセスオブジェクトに移動するでしょう(DAOパターン)。 より多くの例は、HibernateのWikiを参照してください。

1.4.3. デプロイとテスト

このアプリケーションのデプロイのために、Webアーカイブ(WAR)を作成してください。 以下のAntターゲットを 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 記述子を含んでおり、 プロジェクトのベースディレクトリに置かれることを期待されます。

<?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アプリケーションのコンパイルとデプロイの前に、 jsdk.jar という 追加のライブラリが必要なことに注意してください。 これはJavaサーブレットの開発キットです。 もしまだこのライブラリを持っていないなら、Sunのウェブサイトで入手して、 ライブラリディレクトリにコピーしてください。 しかし、これはコンパイルにのみ使用され、WARパッケージからは除外されます。

ビルドとデプロイのために、プロジェクトディレクトリで ant war を呼び出し、 hibernate-tutorial.war ファイルをTomcatの webapp ディレクトリにコピーしてください。 まだTomcatをインストールしていなければ、ダウンロードして、以下のインストールガイドに従ってください。 しかし、このアプリケーションのデプロイするために、Tomcatの設定を変更する必要はありません。

一度デプロイしてTomcatを起動すれば、 http://localhost:8080/hibernate-tutorial/eventmanager で アプリケーションへのアクセスが可能です。 最初のリクエストが作成したサーブレットに渡ったときに、Tomcatのログで Hibernateの初期化処理を確認してください ( HibernateUtil 内の静的初期化ブロックが呼ばれています)。 また、exceptionが発生したなら詳細を確認してください。

1.5. 要約

このチュートリアルでは、簡単なスタンドアローンのHibernateアプリケーションと 小規模のWebアプリケーションを書くための基本を紹介しました。

もうHibernateに自信があれば、リファレンスドキュメントの目次に目を通して、 面白そうだと思うトピックを探してください。 最も頻繁に質問があるのは、トランザクション処理(章 11. トランザクションと並行性)、 フェッチのパフォーマンス(章 19. パフォーマンスの改善)、 APIの使い方(章 10. オブジェクトを扱う)とクエリ (項10.4. 「クエリ」)です。

さらに(特別な)チュートリアルが必要なら、Hibernateウェブサイトを忘れずにチェックしてください。