Hibernate メモ

最終更新日:2005/03/24
ogawa567@oki.com

目次

 1. はじめに
 2. 準備
   2.1 必要な媒体のダウンロード
   2.2 インストール
 3. Hibernateの仕組み
   3.1 O/Rマッピング
 4. ラウンドトリップエンジニアリング
   4.1 DBスキーマの作成
   4.2 middlegenによるマッピングファイルの自動生成
   4.3 hbm2javaによるPOJOの自動生成
   4.4 DAOの作成
   4.5 アプリケーションプログラムの作成
   4.6 hibernate.cfg.xmlファイルの作成
   4.7 実行
 5. Tips
   5.1 コネクションプールの利用
   5.2 Spring Frameworkとの連携

1. はじめに

Hibernateは今流行のO/Rマッピングツールです。O/Rマッピングとはオブジェクトとリレーショナルデータベースの間にある構造上のギャップを埋め、相互に変換することを可能にする技術です。O/Rマッピングツールを使うことにより、アプリケーションからJavaのオブジェクトにアクセスする感覚で、データベースへアクセスできるようになります。

Hibernateは数あるO/Rマッピングツールの中でも広く使われているツールで、多くのサイトで解説がなされています。また、Hibernateには日本語ドキュメントが付属しているので、他のツールに比べて入りやすいということも言えるでしょう。

2. 準備

2.1 必要な媒体のダウンロード

以下のものを準備します。

製品名 バージョン 説明 URL
Hibernate 2.1.8 Hibernate本体 http://www.hibernate.org/
Hibernate Extensions 2.1.3 Hibernate拡張ツール http://www.hibernate.org/
Middlegen 2.1 DBスキーマからhibernateのマッピングファイルを作成するために使用 http://boss.bekk.no/boss/middlegen/
Ant 1.6.2 ビルドツール http://ant.apache.org/
PostgreSQL 7.4.1 データベース http://www.postgresql.org/

2.2 インストール

ダウンロードしたファイルを適当なディレクトリに展開します。以下では、それぞれのインストールディレクトリを以下のように表記します。

製品名 インストールディレクトリの表記
Hibernate ${hibernate.home}
Hibernate Extensions ${hibernate.ext.home}
Middlegen ${middlegen.home}
Ant ${ant.home}

3. Hibernateの仕組み

3.1 O/Rマッピング

HibernateではO/Rマッピングを行うために、マッピングを定義した以下のようなXMLファイルを読み込み、マッピングを行います。

<hibernate-mapping>
  <class name="flight.dao.Flightuser" table="flightuser" >
    <id name="uid" type="int" column="uid" >
      <generator class="sequence">
        <param name="sequence">flightuser_uid_seq</param>
      </generator>
    </id>
    <property name="name" type="java.lang.String" column="name" not-null="true" length="-1" />
  </class>
</hibernate-mapping>

上の例はflight.dao.Flightuserクラスのオブジェクトをflightuserテーブルの1行にマッピングさせるための定義です。<id>はテーブルの主キーを表す要素で、この例ではFlightuserクラスのuidフィールドをflightuserテーブルのuidカラムへ対応させています。<property>は主キー以外の項目を表します。

そして、以下のようなPOJO(通常のJavaクラス)を作成し、

package flight.dao;

public class Flightuser {
    private Integer uid;
    private String name;

    public Integer getUid() {
        return uid;
    }
    public void setUid(Integer uid) {
        this.uid = uid;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

HibernateのAPIを使ってDBへの書き込みや読み込みを行います。

Flightuser user = new Flightuser();
user.setName("hoge");
net.sf.hibernate.Session session = getSession();
session.save(user);

このようにJavaのオブジェクトへアクセスする感覚でDBにアクセスすることができます(検索の場合はHQLが必要なので、SQLの知識が必要ないという訳ではありません)。

さて、Hibernateを使用する場合の典型的なクラス構成について超概念図を書いてみると、

こんな感じになります(青色で示した部分は開発者が作成する部分です)。データベースに行を追加する場合は、ApplicationがPOJOのインスタンスを生成しDAOに渡します。DAOは受け取ったPOJOインスタンスをHibernateへ渡し、Hibernateがマッピングファイルを元にPOJOインスタンスの値をデータベースへ格納します。

データベースからデータを読み込む場合は、ApplicationからDAOへ検索条件を指定し、Hibernateクエリ言語(HQL)によってHibernateへ検索指示を出します。その検索結果がPOJOのインスタンス(及びそのリスト)としてApplicationへ返されます。

4. ラウンドトリップエンジニアリング

POJO、DAO、マッピングファイルはそれぞれテーブル毎に作成する必要があり、これを手作りするのはなかなか面倒です。Hibernateではこれらを自動生成するためのいくつかの方法が用意されています。

Tools for Hibernate 2.xにいくつかの方法が図で示されています。この図を見てわかるように、データベーススキーマからマッピングファイルを生成したり、更にマッピングファイルからPOJOを生成する等、様々な方法があることがわかると思います。また相互変換も可能であることがわかります。

特にPOJOソースをベースにマッピングファイルやデータベーススキーマを生成するアプローチをtop-downといい、データベーススキーマからマッピングファイルやPOJOソースを生成するアプローチをbottom-upといいます。他にもマッピングファイルを起点としたmiddle-out等、やり方は色々あります。

今回はデータベーススキーマを起点として自動生成を行う、bottom-upアプローチについて紹介していきます。

4.1 DBスキーマの作成

今回は以下のようなテーブルをHibernateから利用することにします。

尚、以下ではPostgreSQLをデータベースとして使うことを前提に進めていきます。PostgreSQLの場合のテーブル生成SQLはこのようにしました。

テーブルを生成したら次はDBスキーマからのマッピングを行います。

4.2 middlegenによるマッピングファイルの自動生成

middlegenはDBスキーマを読み込み、それに対応したHibernateのマッピングファイルを自動生成することができる便利なツールです(Hibernateに特化したツールではなく、hibernateはサポートしているO/Rマッピングツールの一つです)。

早速middlegenでマッピングファイルを自動生成してみましょう。middlegenにはAntタスクが用意されているので、これを使うことにします。以下はbuild.xmlの抜粋です。

<taskdef name="middlegen"
         classname="middlegen.MiddlegenTask"
         classpathref="classpath" />

<target name="middlegen" depends="init">
         <middlegen appname="${appname}"
                    prefsdir="${hibernate.dir}"  ←middlegenのコンフィグファイル保存先
                    gui="true"  ←middlegenのコンフィグを行うGUIを表示するかどうか
                    databaseurl="jdbc:postgresql://localhost:5432/flight"
                    driver="org.postgresql.Driver"
                    schema="public"  ←PostgreSQLの場合publicと書かないとシステムテーブルも対象になる
                    catalog=""
                    username="postgres"
                    password="postgres">

                    <!-- テーブルを個別に指定するときは以下のように記述する。
                         記述しなければすべてのテーブルが対象になる
                    <table generate="true" name="Flightinfo"/>
                    <table generate="true" name="Flightuser"/>
                    <table generate="true" name="Reservation"/>
                    -->

                <!-- Hibernate Plugin -->
                <hibernate destination="${hibernate.dir}" package="${daopackage}">
                        <!-- genXDocletTags="true" -->
                        <hibernateDAO getHibernateSession="getSession();"  ←DAOの雛形を作成する
                                      closeHibernateSession="closeSession();" />  
                </hibernate>
        </middlegen>
</target>

middlegenタスクを実行すると、以下の画面が表示されます。

画面上にDBから抽出したテーブルが表示されています。既に列の情報やリレーションが生成されているので、左上の「Generate」ボタンをクリックすれば対応したマッピングファイルが生成されますが、ここで若干の微調整が必要です。

PostgreSQLの場合だと、SEQUENCEにうまく対応していないようなので、このGUIでSEQUENCEを設定してやります。画面上のテーブルを選択し、「主キーの生成方法」を"sequence"とし、予め生成しておいたSEQUENCE名を入力します。

すべて完了したら左上の「Generate」ボタンをクリックします。すると、以下のファイルが生成されます。

以下は生成されたFlightuser.hbm.xmlファイルです。

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 2.0//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd" >
    
<hibernate-mapping>
  <class name="flight.dao.Flightuser" table="flightuser" >
    <id name="uid" type="int" column="uid" >
      <generator class="sequence">
        <param name="sequence">flightuser_uid_seq</param>
      </generator>
    </id>
    <property name="name" type="java.lang.String" column="name" not-null="true" 
      length="-1" />
    <set name="reservations" lazy="true" inverse="true" cascade="none" >
      <key>
        <column name="uid" />
      </key>
      <one-to-many class="flight.dao.Reservation" />
    </set>
  </class>
</hibernate-mapping>

<class>タグの属性を見ると、flight.dao.Flightuserクラスとflightuserテーブルのマッピングが定義されていることがわかると思います。この例では特にこのファイルを編集する必要はありません。

以下は生成されたFlightuserDAOクラスです。

package flight.dao;

import java.io.Serializable;
import java.util.List;

import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;

/**
 * DAO pattern.  
 * No $plugin.mergedir/hibernate-dao-ALL-class-comments.txt found.
 * No $plugin.mergedir/hibernate-dao-flightuser-class-comments.txt found.
 */
public class FlightuserDAO {

    /**
     * Insert the object into the database.
     *
     * @param obj The object to save.
     *
     * @return The primary key of the newly inserted object.
     */
    public Serializable save(Flightuser obj) throws HibernateException {
        Session hibSession = getSession();
        Serializable retval = hibSession.save(obj);
        closeSession();
        return serializable;
    }

    /**
     * Update the object in the database.
     *
     * @param obj The object to update.
     */
    public void update(Flightuser obj) throws HibernateException {
        Session hibSession = getSession();
        hibSession.update(obj);
        closeSession();
    }

    /**
     * Finder: 
     *
     * @param name The search criteria.
     */
    public List findByName(java.lang.String name) throws HibernateException {
        List retval = null;
        Session hibSession = getSession();
        String hql = "from flight.dao.Flightuser aTable WHERE aTable.Name ";
        if (name == null) {
            hql += " IS NULL";
            retval = hibSession.find(hql);

        } else {
            hql += " ?";
            List objects = hibSession.find(hql, String.valueOf(name),
                net.sf.hibernate.type.TypeFactory.basic(java.lang.String.class
                    .getName()));
            retval = objects;
        }
        closeSession();
        return retval;
    }

    // No $plugin.mergedir/hibernate-dao-flightuser-class-code.txt found.

}

save, update, findByNameというメソッドが生成されています。ただし、これらメソッドの中身は後から変更する必要があります。また、middlegenによって生成されるメソッドだけではDAOとして不十分な場合が多いでしょう。このDAO自動生成はあくまでも雛形を作ってくれるもので、基本は自分で作成すると考えた方が良いでしょう。

4.3 hbm2javaによるPOJOの自動生成

無事マッピングファイルが生成されたところで、次はPOJOを自動生成します。POJOの自動生成には、Hibernateの拡張ツールに含まれるhbm2javaを使います。こちらもmiddlegenと同じくAntタスクが用意されているので、

<target name="hbm2java" depends="middlegen">
    <taskdef name="hbm2java"
             classname="net.sf.hibernate.tool.hbm2java.Hbm2JavaTask"
             classpathref="classpath" />
    <hbm2java config="${hibernate.dir}/hibernate.cfg.xml" output="${src}">
         <fileset dir="${hibernate.dir}">
             <include name="**/*.hbm.xml" />
         </fileset>
    </hbm2java>
</target>

このようなターゲットを作成して実行します。以下は生成されたFlightuserクラスです。

package flight.dao;

import java.io.Serializable;
import java.util.Set;

import org.apache.commons.lang.builder.ToStringBuilder;

/** @author Hibernate CodeGenerator */
public class Flightuser implements Serializable {

    /** identifier field */
    private Integer uid;

    /** persistent field */
    private String name;

    /** persistent field */
    private Set reservations;

    /** full constructor */
    public Flightuser(String name, Set reservations) {
        this.name = name;
        this.reservations = reservations;
    }

    /** default constructor */
    public Flightuser() {
    }

    public Integer getUid() {
        return this.uid;
    }

    public void setUid(Integer uid) {
        this.uid = uid;
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Set getReservations() {
        return this.reservations;
    }

    public void setReservations(Set reservations) {
        this.reservations = reservations;
    }

    public String toString() {
        return new ToStringBuilder(this).append("uid", getUid()).toString();
    }

}

4.4 DAOの作成

クラス構成

次に先ほど生成されたDAOの雛形をベースにDAOを作っていきます。さて、ここでそれぞれのDAOにはいくつか共通するコードが出てきます。特にセッション取得やクローズを毎回記述するのは面倒ですので、以下のようにDAOの基底クラスを作成し、共通メソッドを定義することにします。

トランザクション管理

Hibernateでは独自のトランザクションAPIがあり、今回の例でもHibernateのAPIを使うことにします。トランザクションを行うためには、まず以下のようにSessionFactoryを作成する必要があります。

Configuration c = new Configuration().configure();
SessionFactory sessions = c.buildSessionFactory();

ここで作成したSessionFactoryからSessionを取得し、

Session s = sessions.openSession();
Transaction tx = s.beginTransaction();
// 更新系処理
tx.commit();
s.close();

更にTransactionオブジェクトを生成し、トランザクションの開始やコミットを行います。最後はSessionをクローズします(disconnect()とどちらを使うべき・・・?)。

ConfigurationやSessionFactoryはシステム内で一つあれば十分なので、ここではこれらを保持するSessionManagerクラスをシングルトンとして作成することにします(上のクラス図参照)。

package flight.dao;

import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.cfg.Configuration;

/**
 * HibernateのSessionFactoryを保持するシングルトン
 */
public class SessionManager {
    private static SessionManager instance;

    private SessionFactory sessions;

    private SessionManager() throws HibernateException {
        Configuration c = new Configuration();
        sessions = c.buildSessionFactory();
    }

    public static synchronized SessionManager getInstance() {
        if (instance == null) throw new NotInitializedException();
        return instance;
    }

    /**
     * SessionFactoryを初期化する。
     * getInstance()を呼び出す前に必ず一度呼び出す必要がある。
     * @throws HibernateException
     */
    public static synchronized void initialize() throws HibernateException {
        if (instance != null)
            instance = new SessionManager();
    }
    
    public Session getSession() throws HibernateException {
        return sessions.openSession();
    }

    public static class NotInitializedException extends RuntimeException {}
}

そして、このSessionManagerをBaseDAOから参照します。

package flight.common.dao;

import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Transaction;
import flight.dao.SessionManager;

/**
 * 各DAOにSession取得のための簡易メソッドを提供する基底クラス。
 */
public class BaseDAO {
    protected SessionManager manager;
    
    public BaseDAO() {
        manager = SessionManager.getInstance();
    }
    
    /** 
     * HibernateのSessionを取得する
     * @return
     * @throws HibernateException
     */
    public Session getSession() throws HibernateException {
        return manager.getSession();
    }
    
    /** 
     * トランザクションのロールバックを行う
     * @param session
     */
    public void rollback(Transaction tr) {
        try {
            tr.rollback();
        } catch (HibernateException ignore) {}
    }
    
    /**
     * HibernateのSessionを閉じる
     */
    public void closeSession(Session session) {
        if (session != null) {
            try {
                session.close();
            } catch (HibernateException ignore) {}
        }
    }
}

個々のDAO作成

基底クラスを作ったところで、次は個々のDAOの作成(修正)です。自動生成されたDAOにはトランザクション処理が入っていないので、自分で埋め込む必要があります。以下はsaveメソッド(オブジェクトに相当する1行をinsertする)にトランザクション処理を組み込んだ例です。

/**
 * データベースへオブジェクトをinsertする
 */
public void save(Flightuser obj) throws HibernateException {
    Session sess = getSession();
    Transaction tx = null;
    try {
        tx = sess.beginTransaction();
        sess.save(obj);
        tx.commit();
    } catch (HibernateException ex) {
        rollback(tx);
        throw ex;
    } finally {
        closeSession(sess);
    }
}

try...catch...finally節を使ってトランザクション処理を行っています。getSession()等はBaseDAOクラスのメソッドの呼び出しです。

次は検索(select)処理です。検索は主キーによる検索とそれ以外で検索方法が異なります。まずは主キーによる検索の例です。

/**
 * 引数で指定した主キー値にマッチするFlightuserオブジェクトを取得する
 * @param uid ユーザID
 * @throws HibernateException
 */
public Flightuser load(int uid) throws HibernateException {
    Flightuser user = null;
    Session sess = getSession();
    try {
        user = (Flightuser) sess.load(Flightuser.class, new Integer(uid));
    } finally {
        closeSession(sess);
    }
    return user;
}

主キーによる検索ではSession#load()メソッドを使います。第1引数はPOJOのクラスオブジェクトで、第2引数には主キーの値を与えます。主キーがプリミティブ値の場合は上の例のようにラッパークラスのオブジェクトを与えます。

主キー以外の検索では、HQLを使います。HQLはHibernateクエリー言語の略で、SQLに非常によく似た問い合わせ言語です。最も簡単なHQLは

from flight.dao.Flightuser

こうです。このHQLはflightuserテーブルのすべての行をflight.dao.Flightuserオブジェクトのリストとして取得することを意味しています。・・・え?どこからflightuserテーブルが出てきたって?ちょっと思い出してください。Flightuser.hbm.xmlには以下の記述がありました。

<class name="flight.dao.Flightuser" table="flightuser" >

この定義に従ってfilght.dao.Flightuserをfilghtuserテーブルににマッピングしてくれるのですね。ただ、大抵はwhere句等でPOJOインスタンスを参照するので、

from flight.dao.Flightuser as flightuser

としてエイリアスを割り当てます。このエイリアスを使って、

from flight.dao.Flightuser flightuser WHERE flightuser.name = ?

このようにwhere句等で使います。尚、HQLではasキーワードは省略可能なので、上のように書くことができます。実際の検索は以下のように行います。

/**
 * 引数にマッチする名前を持つFlightuserオブジェクトのリストを取得する
 */
public List findByName(String name) throws HibernateException {
    List users = null;
    Session sess = getSession();
    String hql = "from flight.dao.Flightuser flightuser WHERE flightuser.name = ?";
    try {
        users = sess.find(hql, name, Hibernate.STRING);
    } finally {
        closeSession(sess);
    }
    return users;
}

修正を加えたFlightuserDAO.javaは以下のようになりました。

package flight.dao;

import java.util.List;

import net.sf.hibernate.Hibernate;
import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Transaction;
import flight.common.dao.BaseDAO;

/**
 * Flightuserテーブルへのアクセスを行うDAO
 */
public class FlightuserDAO extends BaseDAO {

    /**
     * データベースへオブジェクトをinsertする
     */
    public void save(Flightuser obj) throws HibernateException {
        Session sess = getSession();
        Transaction tx = null;
        try {
            tx = sess.beginTransaction();
            sess.save(obj);
            tx.commit();
        } catch (HibernateException ex) {
            rollback(tx);
            throw ex;
        } finally {
            closeSession(sess);
        }
    }

    /**
     * データベース内のオブジェクトを更新する
     */
    public void update(Flightuser obj) throws HibernateException {
        Session sess = getSession();
        Transaction tx = null;
        try {
            tx = sess.beginTransaction();
            sess.update(obj);
            tx.commit();
        } catch (HibernateException ex) {
            rollback(tx);
            throw ex;
        } finally {
            closeSession(sess);
        }
    }

    /**
     * 引数で指定した主キー値にマッチするFlightuserオブジェクトを取得する
     * @param uid ユーザID
     */
    public Flightuser load(int uid) throws HibernateException {
        Flightuser user = null;
        Session sess = getSession();
        try {
            user = (Flightuser) sess.load(Flightuser.class, new Integer(uid));
        } finally {
            closeSession(sess);
        }
        return user;
    }

    /**
     * 引数にマッチする名前を持つFlightuserオブジェクトのリストを取得する
     */
    public List findByName(String name) throws HibernateException {
        List users = null;
        Session sess = getSession();
        String hql = "from flight.dao.Flightuser flightuser WHERE flightuser.name = ?";
        try {
            users = sess.find(hql, name, Hibernate.STRING);
        } finally {
            closeSession(sess);
        }
        return users;
    }
}

自動生成されたものと比べて大きく変更されました。個人的にはDAOは自動生成させずに、1から作成した方がようような気がします。

自動生成されたDAOの注意点

現時点のmiddlegenツールよって自動生成されるDAOにはいくつか問題があるようで、そのまま使うことができません。自動生成されたDAOをベースにする場合は次の点に注意して下さい。

4.5 アプリケーションプログラムの作成

以下はDAOを利用してDBアクセスを行うクライアントプログラムです。最初にSessionManagerを初期化し、DAOを生成してから各種操作を行っています。

package flight.test;

import java.util.List;

import net.sf.hibernate.HibernateException;
import flight.dao.Flightinfo;
import flight.dao.FlightinfoDAO;
import flight.dao.Flightuser;
import flight.dao.FlightuserDAO;
import flight.dao.Reservation;
import flight.dao.ReservationDAO;
import flight.dao.SessionManager;

public class Test01 {
    private FlightinfoDAO fDao = new FlightinfoDAO();
    private FlightuserDAO uDao = new FlightuserDAO();
    private ReservationDAO rDao = rDao = new ReservationDAO();

    public Test01() {
        // DAOの生成
        fDao = new FlightinfoDAO();
        uDao = new FlightuserDAO();
        rDao = new ReservationDAO();
    }

    /** 新規ユーザを追加する */
    public void addUser(String name) throws HibernateException {
        Flightuser user = new Flightuser();
        user.setName(name);
        uDao.save(user);
    }

    /**
     * 名前がnameに一致するユーザを取得する
     */
    public Flightuser[] findUsersByName(String name) throws HibernateException {
        List users = uDao.findByName(name);
        return (Flightuser[]) users.toArray(new Flightuser[users.size()]);
    }

    /** 指定したユーザの予約情報を取得する。
     * @param uid ユーザID */
    public Reservation[] findReservations(int uid) throws HibernateException {
        List rsvs = rDao.findByUid(uid);
        return (Reservation[]) rsvs.toArray(new Reservation[rsvs.size()]);
    }

    /**
     * 新規予約を行う
     * @param uid ユーザID
     * @param fid フライトID
     * @param number 座席数
     */
    public void createNewReservation(int uid, int fid, int number)
        throws HibernateException {
        Flightuser user = uDao.load(uid);
        Flightinfo info = fDao.load(fid);
        Reservation rsv = new Reservation();
        rsv.setNumber(number);
        rsv.setFlightinfo(info);
        rsv.setFlightuser(user);
        rDao.save(rsv);
    }

    public static void main(String[] args) throws HibernateException {
        SessionManager.initialize();
        Test01 test = new Test01();

        // ユーザ追加
        test.addUser("foo" + System.currentTimeMillis());

        // ユーザ検索
        Flightuser[] users = test.findUsersByName("ogawa");
        for (int i = 0; i < users.length; i++) {
            System.out.println("ユーザID:" + users[i].getUid());
        }

        // 予約検索
        Reservation[] rsvs = test.findReservations(5);
        for (int i = 0; i < rsvs.length; i++) {
            System.out.println("予約ID:" + rsvs[i].getRid());
        }

        // 新規予約
        test.createNewReservation(1, 100, 4);
    }
}

4.6 hibernate.cfg.xmlファイルの作成

最後にHibernateの設定ファイルの作成です。このファイルではどのデータベースにアクセスするかといった、Hibernate全般の設定を記述します。

<?xml version='1.0' ?>

<!DOCTYPE hibernate-configuration PUBLIC
      "-//Hibernate/Hibernate Configuration DTD 2.0//EN"
      "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="connection.url">jdbc:postgresql://localhost/flight</property>
    <property name="connection.username">postgres</property>
    <property name="connection.password">postgres</property>
    <property name="connection.driver_class">org.postgresql.Driver</property>
    <property name="dialect">net.sf.hibernate.dialect.PostgreSQLDialect</property>
    <property name="show_sql">true</property>
    <!-- マッピングファイル -->
    <mapping resource="flight/dao/Flightinfo.hbm.xml"/>
    <mapping resource="flight/dao/Flightuser.hbm.xml"/>
    <mapping resource="flight/dao/Reservation.hbm.xml"/>
  </session-factory>
</hibernate-configuration>

上のように<property>要素でSessionFactoryに関する設定を行います。データベースのURLやユーザ名、パスワード等を記述します。Hibernateはこのファイル(や、*.hbm.xml)を実行時にリソースとして読み込み、これらの設定に基づいて動作します。

ファイル名はhibernate.cfg.xmlとしておきましょう。こうすればHibernateにこのファイルを読み込ませるためにファイル名(リソース名)を指定しなくて済むからです。

middlegenによって生成したマッピングファイル(*.hbm.xml)もこのファイルですべて指定しておきます。<mapping>要素のresource属性にリソースの場所を指定します。このとき各マッピングファイルに対応するPOJOのクラス階層に従ってパスを記述する必要があります。

4.7 実行

実行の前にマッピングファイル(*.hbm.xml)とhibernate.cfg.xmlファイルをクラスパスの通っている場所へコピーします。hibernate.cfg.xmlはクラスパスのルートに、マッピングファイルは対応するPOJOクラスと同じディレクトリにコピーします。

必要なJarファイルをクラスパスに追加した後、Test01を実行するとユーザの追加と検索が行われるはずです。JDBCドライバをクラスパスに含めることを忘れないで下さい。

5. Hibernateを使う上での注意点

遅延ローディング

one-to-manyの関連では、one側のPOJOがmany側のPOJOをコレクションとして保持します。下図はFlightuser(one)とReservation(many)の場合です。

もし遅延ローディングを有効にしなかった場合、Flightuserオブジェクトをロードすると、Flightuserに関連付けられたReservationも一緒にロードされます。

しかし単にFlightuserの名前を参照するだけならばReservationまでロードするのは明らかに無駄です。そのため、Hibernateでは関連オブジェクトを必要になった時点でロードするための仕組みとして遅延ローディングをサポートしています。遅延ローディングを有効にするには、マッピングファイルでone側のコレクションプロパティ(この場合はSet)に以下の属性を設定します。

lazy="true"

これでFlightuserをロードした時点ではResevationはロードされなくなり、必要になった時点でロードが行われるようになります。この例では、Flightuser#getReservations()が呼び出された時点です。

必要になった時点でロードされるということは、その時点でSQLが発行されることになるのですが、既にHibernateのSessionを閉じている時点でこの操作を行うと、例外がスローされてしまいます。例えば、以下のごく自然に思えるコードで、

Flightuser user = uDao.load(uid);
Set rsvs = user.getReservations();

こんな例外がスローされます。

[java] net.sf.hibernate.LazyInitializationException: Failed to lazily initialize a collection
        - no session or session was closed

userをロードするために使用したSessionは既にload()メソッド内で閉じられているため、その後のResevation取得で例外が発生してしまうのです。

遅延ローディングを無効にすれば解決できますが、明らかに性能的に問題がありそうです。WebアプリでHibernateを使う場合は、Open Session in Vewパターンを採用することによってこの問題を回避できるようです。Sessionのオープン、クローズを行うサーブレットフィルタを使い、一度のリクエスト/レスポンスで一つのSessionを使うという方法です。

性能

Hibernateを使うことによって面倒なJDBC APIを直接使わなくて良くなり、オブジェクト指向的な考え方でDBへアクセスするコードを記述できるようになったわけですが、性能はどうなのでしょうか?

例えば4.5で示したクライアントプログラムを実行するとどんなSQLが実際に生成されるのでしょうか?4.6のhibernate.cfg.xmlではshow_sql=trueというプロパティを与えています。このプロパティを与えることによって、標準出力に実際に発行されるSQLが出力されます。

4.5のプログラムを実行すると以下のSQLが発行されました。

// ユーザ追加
select nextval ('flightuser_uid_seq')
insert into flightuser (name, uid) values (?, ?)

// ユーザ検索
select flightuser0_.uid as uid, flightuser0_.name as name from flightuser flightuser0_ 
       where (flightuser0_.name=? )

// 予約検索
select reservatio0_.rid as rid, reservatio0_.number as number, reservatio0_.fid as fid, 
       reservatio0_.uid as uid from reservation reservatio0_ where (reservation.uid=? )
select flightinfo0_.fid as fid0_, flightinfo0_.fromport as fromport0_, 
       flightinfo0_.toport as toport0_, flightinfo0_.seatnum as seatnum0_,
       flightinfo0_.freenum as freenum0_ from flightinfo flightinfo0_ where flightinfo0_.fid=?
select flightuser0_.uid as uid0_, flightuser0_.name as name0_ from flightuser flightuser0_ 
       where flightuser0_.uid=?
select flightinfo0_.fid as fid0_, flightinfo0_.fromport as fromport0_, 
       flightinfo0_.toport as toport0_, flightinfo0_.seatnum as seatnum0_,
       flightinfo0_.freenum as freenum0_ from flightinfo flightinfo0_ where flightinfo0_.fid=?
select flightuser0_.uid as uid0_, flightuser0_.name as name0_ from flightuser flightuser0_ 
       where flightuser0_.uid=?
select flightinfo0_.fid as fid0_, flightinfo0_.fromport as fromport0_,
       flightinfo0_.toport as toport0_, flightinfo0_.seatnum as seatnum0_,
       flightinfo0_.freenum as freenum0_ from flightinfo flightinfo0_ 
       where flightinfo0_.fid=?
select flightuser0_.uid as uid0_, flightuser0_.name as name0_ from flightuser flightuser0_ 
       where flightuser0_.uid=?

// 新規予約
select flightuser0_.uid as uid0_, flightuser0_.name as name0_ from flightuser flightuser0_ 
       where flightuser0_.uid=?
select flightinfo0_.fid as fid0_, flightinfo0_.fromport as fromport0_, 
       flightinfo0_.toport as toport0_, flightinfo0_.seatnum as seatnum0_, 
       flightinfo0_.freenum as freenum0_ from flightinfo flightinfo0_ 
       where flightinfo0_.fid=?
select nextval ('reservation_rid_seq')
insert into reservation (number, fid, uid, rid) values (?, ?, ?, ?)

ユーザ追加とユーザ検索は予想通りでしたが、予約検索ではselect文が7回発行されています。予約検索ではReservationテーブルからあるユーザIDにマッチする予約をすべて取得する処理を行っています。別の言い方をすると、1対多の関係にあるテーブルの多側のレコードを取得する処理を行っています。

Hibernateでは多側のレコードを取得しようとすると、1側の関連のあるレコードを取得するためのSQLが発行されるようです。今回の例ではReservationテーブルから合計3つのレコードを取得し、その3つそれぞれに対してFlightInfo, FlightUserテーブルから関連レコードを取得するためのSQLが発行されたために、合計7回のselect文が発行されたというわけです。

最後の新規予約ではReservationクラスのオブジェクトを生成するために、FlightinfoとFlightuserクラスのオブジェクトが必要のため、まずそれぞれのオブジェクトをロードしてからReservationをinsertしています。この2つのオブジェクトのロードのために、insertの前にselect文が2回発行されています。

このようにHibernateを使うことによって、オブジェクト指向の考え方でDB操作を行うことが可能になったわけですが、その一方JDBC APIを直接利用する場合ならば発行されるはずもなかったSQLが発行されてしまうようです(使い方の問題・・・?)。JDBCが隠蔽されている分、デバッグ作業は逆に大変になるのかもしれません。特にHibernateが(仕組み上)どういうSQLを発行するのか、きちんと理解しておく必要がありそうです。

5. Tips

5.1 コネクションプールの利用

Hibernateではデフォルトで以下の3つのコネクションプーリングの実装が利用できます。

このうち、C3P0を使う方法はHibernateのドキュメントに書いてありますが、残りの2つは具体的な記述がありません。Webを調べてみると、DBCPを使う場合は以下のプロパティをhibernate.cfg.xmlに追加すればいいようです。

<property name="hibernate.dbcp.maxActive">10</property>
<property name="hibernate.dbcp.maxIdle">10</property>
<property name="hibernate.dbcp.maxWait">180000</property>
<property name="hibernate.dbcp.whenExhaustedAction">1</property>

DBCPが有効になったかどうかは実行時に出力されるログで判断できます。このプロパティを追加して実行すると、以下のようなログが出力されます(Log4jのログレベルをINFO以上にすること)。

DBCP using driver: org.postgresql.Driver at URL: jdbc:postgresql://localhost/flight
Connection properties: {user=postres, password=postgres}
DBCP prepared statement pooling disabled

このメッセージを見るとprepared statementのキャッシュが有効になっていないとあります。Hibernateのドキュメントにも、hibernate.dbcp.ps.*が設定されるとこのキャッシュが有効になりお勧めと書いてあるので、以下のように両方追加してみると、

<property name="hibernate.dbcp.maxActive">10</property>
<property name="hibernate.dbcp.maxIdle">10</property>
<property name="hibernate.dbcp.maxWait">180000</property>
<property name="hibernate.dbcp.whenExhaustedAction">1</property>
<property name="hibernate.dbcp.ps.maxActive">10</property>
<property name="hibernate.dbcp.ps.maxIdle">10</property>
<property name="hibernate.dbcp.ps.maxWait">180000</property>
<property name="hibernate.dbcp.ps.whenExhaustedAction">1</property>

prepared statementのキャッシュが有効になったようです。

DBCP prepared statement pooling enabled

5.2 Spring Frameworkとの連携

Spring FrameworkはオープンソースのJavaアプリケーションフレームワークで、EJBコンテナと対比して軽量コンテナと呼ばれるものの一つです。EJBでは、

といった問題点があります。これらを解決してくれるのが軽量コンテナで、Springもその一つです。このSpringにはHibernateとの連携をサポートするための機能があるため、Hibernate単独で使用するよりもSpringとの組み合わせで使用した方が、よりシンプルな構造になります。

特にSpring FrameworkはAOP(アスペクト指向プログラミング)の機能があるため、DAOクラスで書いていたトランザクション管理等はすべてSpringに任せるようなことが可能です。HibernateとSpringの組み合わせた場合、各クラスの実装がどのようになるのかは、今後Springについてまとめる機会があったらそのときに書こうと思います。お楽しみに!

資料室へ戻る


Copyright (c) 2005 OKI Software Co., Ltd.