第9章 永続データの操作

9.1. 永続オブジェクトの作成

オブジェクト(エンティティ・インスタンス)は特定の Session に対して 一時的 または 永続的 です。 新しくインスタンス化されたオブジェクトは、当然一時的です。 Sessionは一時的なインスタンスをセーブするサービス(つまり永続化)を行います:

DomesticCat fritz = new DomesticCat();
fritz.setColor(Color.GINGER);
fritz.setSex('M');
fritz.setName("Fritz");
Long generatedId = (Long) sess.save(fritz);
DomesticCat pk = new DomesticCat();
pk.setColor(Color.TABBY);
pk.setSex('F');
pk.setName("PK");
pk.setKittens( new HashSet() );
pk.addKitten(fritz);
sess.save( pk, new Long(1234) );

引数が1個の save() は、fritz にユニークな識別子を生成し、代入します。 引数が2個の方は、与えられた識別子を使い pk を永続化しようとします。 ビジネス上の意味を持つ主キーを作成するために使えてしまうので、 一般に引数が2個の方はおすすめしません。 しかしBMPエンティティ・ビーンの永続化にHibernateを使うような特殊な状況なら、引数が2個の方は有用です。

外部キーのカラムに対して NOT NULL 制約がなければ、 好きな順序で関連オブジェクトを永続化できます。 外部キー制約に違反するリスクは全くありません。 しかし、間違った順序でオブジェクトを save() してしまうと、 NOT NULL 制約に違反するかもしれません。

9.2. オブジェクトのロード

もし識別子がわかっていれば、Sessionload() メソッドを使い、 永続インスタンスを復元できます。 1番目のバージョンはクラス・オブジェクトを引数に取り、 新しくインスタンス化したオブジェクトに状態をロードします。 インスタンスを引数に取るもう1つのバージョンは、 HibernateをBMPエンティティ・ビーンと一緒に使う目的のためなら、特に有用です。 また他にも使いみちがあるかもしれません。(DIYインスタンス・プールなど)

Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// プリミティブ型の識別子はラップする必要があります
long pkId = 1234;
DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
Cat cat = new DomesticCat();
// pkの状態をcatにロードします
sess.load( cat, new Long(pkId) );
Set kittens = cat.getKittens();

該当する行が存在しなければ、load() は復帰不可能な例外をスローすることに注意してください。 クラスがプロキシでマッピングされていれば、load() は初期化されていないプロキシを返し、 オブジェクトのメソッドを起動するまでは、実際にデータベースに問い合わせません。 実際にデータベースからロードせずに、オブジェクトへの関連を作成したければ、この振る舞いはとても役に立ちます。

該当する行が存在することが確実でなければ、get() メソッドを使うべきです。 これはすぐにデータベースに問い合わせ、該当する行が存在しなければnullを返します。

Cat cat = (Cat) sess.get(Cat.class, id);
if (cat==null) {
    cat = new Cat();
    sess.save(cat, id);
}
return cat;

SQLの SELECT ... FOR UPDATE を使い、オブジェクトをロードすることもできます。 Hibernateの LockMode についての議論は、次節を見てください。

Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);

FOR UPDATE では、関連インスタンスや含んでいるコレクションは 選択 されない ことに注意してください。

refresh() メソッドを使うと、オブジェクトやすべてのコレクションをいつでもリロードできます。 データベース・トリガを使って、オブジェクトのプロパティのいくつかが初期化されるときに、これは役に立ちます。

sess.save(cat);
sess.flush(); // SQLのINSERTを強制します
sess.refresh(cat); // 状態を再び読み込みます(トリガの実行後に)

9.3. クエリの実行

探すオブジェクトの識別子がわからなければ、 Sessionfind() メソッドを使ってください。 Hibernateはシンプルかつ強力なオブジェクト指向クエリ言語を用意しています。

List cats = sess.find(
    "from Cat as cat where cat.birthdate = ?",
    date,
    Hibernate.DATE
);

List mates = sess.find(
    "select mate from Cat as cat join cat.mate as mate " +
    "where cat.name = ?",
    name,
    Hibernate.STRING
);

List cats = sess.find( "from Cat as cat where cat.mate.bithdate is null" );

List moreCats = sess.find(
    "from Cat as cat where " + 
    "cat.name = 'Fritz' or cat.id = ? or cat.id = ?",
    new Object[] { id1, id2 },
    new Type[] { Hibernate.LONG, Hibernate.LONG }
);

List mates = sess.find(
    "from Cat as cat where cat.mate = ?",
    izi,
    Hibernate.entity(Cat.class)
);

List problems = sess.find(
    "from GoldFish as fish " +
    "where fish.birthday > fish.deceased or fish.birthday is null"
);

find() の2番目の引数は、オブジェクトまたはオブジェクトの配列を受け取ります。 3番目の引数はHibernate型またはHibernate型の配列を受け取ります。 これらは、与えられたオブジェクトを ? クエリ・プレースホルダにバインドするために使われます。 (JDBCの PreparedStatement のINパラメータへマッピングします。) JDBCと同じように、文字列操作ではなくこのバインディング機構を使うべきです。

ほとんどの組み込み型へのアクセスを提供するために、 net.sf.hibernate.type.Type のインスタンスとして、 Hibernate のクラスはstaticメソッドと定数をたくさん用意しています。

クエリが非常に多くのオブジェクトを返すにもかかわらず、それらすべてを使うわけではないならば、 iterate() メソッドを使うと良いパフォーマンスが得られます。 これは java.util.Iterator を返します。 イテレータは初期のSQLクエリから返される識別子を使い、 要求に応じてオブジェクトをロードします。(全部でn+1のセレクトになります。)

// idを取得します
Iterator iter = sess.iterate("from eg.Qux q order by q.likeliness"); 
while ( iter.hasNext() ) {
    Qux qux = (Qux) iter.next();  // オブジェクトを取得します
    // クエリでは表現できない何か
    if ( qux.calculateComplicatedAlgorithm() ) {
        // 現在のインスタンスを削除します
        iter.remove();
        // 残りを実行する必要はありません
        break;
    }
}

あいにく java.util.Iterator は何も例外を定義していないので、 発生するSQLやHibernateの例外は LazyInitializationExceptionRuntimeException のサブクラス)でラップされます。

多くのオブジェクトがすでにSessionでロードされてキャッシュされているか、 クエリのリザルトが同じオブジェクトを何度も含むなら、 iterate() メソッドもよく動作します。 (データがキャッシュされたりイテレートされたりしなければ、 ほとんどの場合 find() の方がより高速です。) 以下は iterate() を使ってコールすべきクエリの例です:

Iterator iter = sess.iterate(
    "select customer, product " + 
    "from Customer customer, " +
    "Product product " +
    "join customer.purchases purchase " +
    "where product = purchase.product"
);

find() を使って上記のクエリをコールすると、 同じデータを何度も含む非常に大きなJDBC ResultSet が返されます。

Hibernateクエリはオブジェクトのタプルを返すことがあります。 その場合、各タプルは配列として返されます:

Iterator foosAndBars = sess.iterate(
    "select foo, bar from Foo foo, Bar bar " +
    "where bar.date = foo.date"
);
while ( foosAndBars.hasNext() ) {
    Object[] tuple = (Object[]) foosAndBars.next();
    Foo foo = tuple[0]; Bar bar = tuple[1];
    ....
}

9.3.1. スカラ・クエリ

select 句の中で、クラスのプロパティを指定できます。 さらにSQLの集計関数もコールできます。プロパティや集計は「スカラ」です。

Iterator results = sess.iterate(
        "select cat.color, min(cat.birthdate), count(cat) from Cat cat " +
        "group by cat.color"
);
while ( results.hasNext() ) {
    Object[] row = results.next();
    Color type = (Color) row[0];
    Date oldest = (Date) row[1];
    Integer count = (Integer) row[2];
    .....
}
Iterator iter = sess.iterate(
    "select cat.type, cat.birthdate, cat.name from DomesticCat cat"
);
List list = sess.find(
    "select cat, cat.mate.name from DomesticCat cat"
);

9.3.2. クエリ・インターフェイス

リザルトセットに境界(復元したい行の最大数や復元したい最初の行)を指定する必要があれば、 net.sf.hibernate.Query のインスタンスを取得すべきです:

Query q = sess.createQuery("from DomesticCat cat");
q.setFirstResult(20);
q.setMaxResults(10);
List cats = q.list();

マッピング・ドキュメントの中で、名前付きのクエリを定義することもできます (マークアップとして翻訳される文字を含んでいれば CDATA セクションを使うことを忘れないでください)。

<query name="eg.DomesticCat.by.name.and.minimum.weight"><![CDATA[
    from eg.DomesticCat as cat
        where cat.name = ?
        and cat.weight > ?
] ]></query>
Query q = sess.getNamedQuery("eg.DomesticCat.by.name.and.minimum.weight");
q.setString(0, name);
q.setInt(1, minWeight);
List cats = q.list();

クエリ・インターフェイスでは名前付きのパラメータを使えます。 名前付きのパラメータとは、クエリ文字列中の :name の形式の識別子のことを言います。 Query には、名前付きのパラメータやJDBCスタイルの ? パラメータにバインドするためのメソッドが用意されています。 JDBCとは違い、Hibernateの数パラメータは0から始まります。 名前付きパラメータの利点は、以下のようなものです:

  • クエリ文字列中に現れる順序に影響されない。

  • 同じクエリ中に複数回現れてもよい。

  • 名前で意味を表現できる。

// 名前付きパラメータ(おすすめ)
Query q = sess.createQuery("from DomesticCat cat where cat.name = :name");
q.setString("name", "Fritz");
Iterator cats = q.iterate();
// 位置バラメータ
Query q = sess.createQuery("from DomesticCat cat where cat.name = ?");
q.setString(0, "Izi");
Iterator cats = q.iterate();
// 名前付きパラメータのリスト
List names = new ArrayList();
names.add("Izi");
names.add("Fritz");
Query q = sess.createQuery("from DomesticCat cat where cat.name in (:namesList)");
q.setParameterList("namesList", names);
List cats = q.list();

9.3.3. スクローラブル・イテレーション

JDBCドライバがスクローラブル ResultSet に対応していれば、 Query インターフェイスを使って ScrollableResults を取得できます。 これを使うと、クエリのリザルトを柔軟にナビゲーションできます。

Query q = sess.createQuery("select cat.name, cat from DomesticCat cat " +
                            "order by cat.name");
ScrollableResults cats = q.scroll();
if ( cats.first() ) {

    // catを名前のアルファベット順で並べたリストの、各ページの最初の名前を見つけます
    firstNamesOfPages = new ArrayList();
    do {
        String name = cats.getString(0);
        firstNamesOfPages.add(name);
    }
    while ( cats.scroll(PAGE_SIZE) );

    // catの最初のページを取得します
    pageOfCats = new ArrayList();
    cats.beforeFirst();
    int i=0;
    while( ( PAGE_SIZE > i++ ) && cats.next() ) pageOfCats.add( cats.get(1) );

}

9.3.4. コレクションのフィルタリング

コレクションの フィルタ は、 永続コレクションや配列に適用されるクエリの特別なタイプです。 クエリ文字列から、現時点のコレクション要素を意味する this

Collection blackKittens = session.filter(
    pk.getKittens(), "where this.color = ?", Color.BLACK, Hibernate.enum(Color.class)
);

返されるコレクションはbagです。

フィルタには from 句が必要ありません(あってもかまいません)。 またフィルタは、コレクション要素そのものを返せます。

Collection blackKittenMates = session.filter(
    pk.getKittens(), "select this.mate where this.color = eg.Color.BLACK"
);

9.3.5. Criteriaクエリ

HQLは極めて強力ですが、Javaコードの中に文字列として埋め込むのではなく、 オブジェクト指向のAPIを使って、動的にクエリを組み立てる方法を好む人もいます。 そういう人のために、Hibernateでは直感的な Criteria クエリAPIが用意されています。

Criteria crit = session.createCriteria(Cat.class);
crit.add( Expression.eq("color", eg.Color.BLACK) );
crit.setMaxResults(10);
List cats = crit.list();

SQLライクな構文が嫌いなら、多分これがHibernateを始める最も簡単な方法です。 このAPIはHQLより簡単に拡張できます。 アプリケーションで Criterion インターフェイスの独自の実装を用意することもできます。

9.3.6. ネイティブSQLのクエリ

createSQLQuery() を使い、SQLの中でクエリを表現することもできます。 その場合、中括弧でSQLのエイリアスを囲まなければなりません。

List cats = session.createSQLQuery(
    "SELECT {cat.*} FROM CAT {cat} WHERE ROWNUM<10",
    "cat",
    Cat.class
).list();
List cats = session.createSQLQuery(
    "SELECT {cat}.ID AS {cat.id}, {cat}.SEX AS {cat.sex}, " +
           "{cat}.MATE AS {cat.mate}, {cat}.SUBCLASS AS {cat.class}, ... " +
    "FROM CAT {cat} WHERE ROWNUM<10",
    "cat",
    Cat.class
).list()

Hibernateクエリと同じように、SQLクエリでも名前付きパラメータと位置パラメータが使えます。

9.4. オブジェクトの更新

9.4.1. 同じSessionでの更新

トランザクショナル永続インスタンス  ( Session でロード、セーブ、作成、 クエリ実行されたオブジェクト)はアプリケーションで操作でき、 Sessionフラッシュ されるときに 永続状態への変更が永続化されます。 (この章の終わりに議論します。) そのため、オブジェクトの状態を更新する最も簡単な方法は、 Session がオープンしている間に load() して直接操作する方法です:

DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) );
cat.setName("PK");
sess.flush();  // catへの変更は自動的に検知され永続化されます

同じSessionで(オブジェクトをロードするための)SQL SELECT と (更新された状態を永続化するための)SQL UPDATE の両方が必要となるため、 このプログラミング・モデルはときどき非効率です。 そのため、Hibernateは代わりの方法を用意しています。

9.4.2. 関連付けをやめたオブジェクトの更新

多くのアプリケーションでは、1つのトランザクションでオブジェクトを復元して、 操作のためにUI層へ送って、新しいトランザクションで変更をセーブする必要があります。 (同時並行性の高い環境でこの種の方法を使うアプリケーションは、 普通トランザクションの分離を確実にするためにバージョン・データを使います。) この方法は、前の節で述べたプログラミング・モデルとはかなり異なったプログラミング・モデルが必要です。 Hibernateは Session.update() メソッドを用意することで、 このモデルに対応しています。

// 最初のSessionで
Cat cat = (Cat) firstSession.load(Cat.class, catId);
Cat potentialMate = new Cat();
firstSession.save(potentialMate);

// アプリケーションのより高い層で
cat.setMate(potentialMate);

// その後、新しいSessionで
secondSession.update(cat);  // catの更新
secondSession.update(mate); // mateの更新

アプリケーションが更新を行おうとするとき、 識別子 catId を持つ CatsecondSession ですでにロードされていれば、例外がスローされます。

与えられた一時的なインスタンスから到達可能な一時的なインスタンスを、 アプリケーションは個別に update() すべきです。 そしてそれは状態も更新したいときに 限ります 。 (ライフサイクル・オブジェクトを除く。後で議論します。)

Hibernateのユーザは新しい識別子を生成して一時的なインスタンスをセーブしたり、 現在の識別子に関連する永続状態を更新する汎用的なメソッドをリクエストしていました。 今ではこの機能を実装する saveOrUpdate() メソッドが用意されています。

Hibernateは識別子(またはバージョンかタイムスタンプ)プロパティの値で、 「新しい」(セーブされていない)インスタンスと「既存の」 (以前のSessionでセーブまたはロードされた)インスタンスを区別します。 <id>(または <version>, または <timestamp> )の unsaved-value 属性のマッピングで、 どの値が「新しい」インスタンスを表すかを指定します。

<id name="id" type="long" column="uid" unsaved-value="null">
    <generator class="hilo"/>
</id>

unsaved-value は以下の値を取ります:

  • any - 常にセーブする

  • none - 常に更新する

  • null - 識別子がnullのときにセーブする(これがデフォルトです)

  • 妥当な識別子の値 - 識別子がnullか値が与えられたときにセーブする

  • undefined - versiontimestamp のデフォルトで、そのとき識別子がチェックされる

// 最初のSessionで
Cat cat = (Cat) firstSession.load(Cat.class, catID);

// アプリケーションのより高い層で
Cat mate = new Cat();
cat.setMate(mate);

// その後、新しいSessionで
secondSession.saveOrUpdate(cat);   // 既存の状態を更新します(catはnullでないidを持っています)
secondSession.saveOrUpdate(mate);  // 新しいインスタンスをセーブします(mateはnullのidを持っています)

新しいユーザには、saveOrUpdate() の使いみちと意味が 混乱しているように思われるかもしれません。 あるSessionのインスタンスを他の新しいSessionで使おうとしない限りは、 update()saveOrUpdate() を使う必要はありません。 これらのメソッドのどちらも、アプリケーション全体を通して全く使わないユーザもいるでしょう。

普通 update()saveOrUpdate() は、 以下のようなシナリオで使われます:

  • アプリケーションが最初のSessionで、オブジェクトをロードする

  • オブジェクトがUI層へ渡される

  • オブジェクトにいくつか更新が行われる

  • オブジェクトがビジネス・ロジック層に返される

  • アプリケーションが2番目のSessionで update() をコールして、 これらの更新を永続化する

saveOrUpdate() は以下のことを行います:

  • オブジェクトがこのSessionですでに永続化されていれば、何もしない

  • オブジェクトに識別子プロパティがなければ、save() する

  • オブジェクトの識別子が unsaved-value で指定されたcriteriaにマッチすれば、 save() する

  • オブジェクトがバージョン付けされていれば。 ( version または timestamp )、 バージョンが unsaved-value="undefined" (デフォルト値)でない限り、 バージョンが識別子のチェックに優先する

  • もしSessionに関連付けられている他のオブジェクトが同じ識別子を持つなら、例外がスローされる

最後のケースは、 saveOrUpdateCopy(Object o) を使うことで避けられます。 このメソッドは、与えられたオブジェクトの状態を、同じ識別子を持つ永続オブジェクトにコピーします。 現在のSessionに関連付けられた永続インスタンスが存在しなければ、ロードされます。 このメソッドは永続インスタンスを返します。 与えられたインスタンスが、セーブされていないかデータベースに存在しなければ、 Hibernateはそれをセーブして、新しく永続化したインスタンスとして返します。 そうでなければ、与えられたインスタンスはSessionに関連付けられません。 オブジェクトの切り離しを使うほとんどのアプリケーションでは、 saveOrUpdate()saveOrUpdateCopy() の両方のメソッドが必要です。

9.4.3. 関連付けをやめたオブジェクトの再関連付け

lock() メソッドを使えば、 アプリケーションを新しいSessionで更新されていないオブジェクトに、 再び関連付けることができます。

// 単純に再接続する:
sess.lock(fritz, LockMode.NONE);
// バージョン・チェックを行い、再接続します:
sess.lock(izi, LockMode.READ);
// SELECT ... FOR UPDATEを使ってバージョン・チェックを行い、再接続します
sess.lock(pk, LockMode.UPGRADE);

9.5. 永続オブジェクトの削除

Session.delete() はデータベースからオブジェクトの状態を削除します。 もちろんアプリケーションがまだそれへの参照を保持しているかもしれません。 そのため delete() は永続インスタンスを一時的にするものと考えるのが一番です。

sess.delete(cat);

Hibernateクエリ文字列を delete() に渡すと、 多くのオブジェクトを一度に削除できます。

今ではもう、外部キー制約に違反するリスクなしで、 好きな順序でオブジェクトを削除することができます。 もちろんオブジェクトを間違った順序で削除すると、 外部キーの NOT NULL 制約に違反する可能性はまだあります。

9.6. フラッシュ

JDBCコネクションの状態をメモリ内部のオブジェクトの状態と同期する必要があるSQL文を Session が実行することがたまにあります。 この処理 フラッシュ はデフォルトでは以下のポイントで起こります。

  • find()iterate() のいくつかの起動から

  • net.sf.hibernate.Transaction.commit() から

  • Session.flush() から

SQL文は以下の順番で発行されます

  1. すべてのエンティティの挿入は Session.save() を使って、 対応するオブジェクトと同じ順序でセーブされる

  2. すべてのエンティティの更新

  3. すべてのコレクションの削除

  4. すべてのコレクション要素の削除、更新、挿入

  5. すべてのコレクションの挿入

  6. すべてのエンティティの削除(Session.delete() を使い 対応するオブジェクトが削除されるのと同じ順序で)

(例外は native ID生成を使い、 それらがセーブされるときにオブジェクトが挿入される場合です。)

明示的に flush() されるときを除いて、 いつ Session がJDBCコールを実行するかの絶対的な保証はなく、 実行の 順序 だけが保証されます。 しかしHibernateは Session.find(..) メソッドが、 古いデータや間違ったデータを返すことは絶対にないことを保証しています。

フラッシュがそう頻繁に起こらないように、デフォルトの振る舞いを変更することはできます。 FlushMode クラスは3つの異なったモードを定義しています。 これは「読み込みのみ」トランザクションの場合に最も有用です。 それは(非常に)わずかなパフォーマンスの改善のために使われるかもしれません。

sess = sf.openSession();
Transaction tx = sess.beginTransaction();
sess.setFlushMode(FlushMode.COMMIT); // クエリが古いデータを返すことを許します
Cat izi = (Cat) sess.load(Cat.class, id);
izi.setName(iznizi);
// クエリをいくつか実行します....
sess.find("from Cat as cat left outer join cat.kittens kitten");
// iziへの変更はフラッシュされません
...
tx.commit(); // フラッシュが起こります

9.7. Sessionの終了

Sessionの終了には4つの明確なフェーズがあります:

  • Sessionをフラッシュする

  • トランザクションをコミットする

  • Sessionをクローズする

  • 例外に対処する

9.7.1. Sessionのフラッシュ

偶然 Transaction APIを使うことになったとしても、 このステップを心配する必要はありません。 これはトランザクションがコミットされるときに、暗黙的に実行されます。 そうでなければ、すべての変更がデータベースと同期を取ることを確実にするために、 Session.flush() をコールすべきです。

9.7.2. データベース・トランザクションのコミット

Hibernateの Transaction APIを使うなら、このようになります:

tx.commit(); // Sessionをフラッシュしトランザクションをコミットします

もしJDBCトランザクションを独自に管理するなら、 JDBCコネクションを手作業で commit() すべきです。

sess.flush();
sess.connection().commit();  // JTAデータソースには必要ありません

もし変更をコミット しないと決めたなら:

tx.rollback();  // トランザクションをロールバックします]></programlisting>

            <para>
                or:
            </para>

            <programlisting><![CDATA[// JTAデータソースには必要ありませんが、そうでなければ重要です
sess.connection().rollback();

トランザクションをロールバックするなら、 Hibernateの内部状態の整合性が取れていることを確実にするために、 現在のSessionを即座にクローズして捨てるべきです。

9.7.3. Sessionのクローズ

Session.close() へのコールはSessionの終了をマークします。 JDBCコネクションがSessionから放棄されることが close() の主な意味です。

tx.commit();
sess.close();
sess.flush();
sess.connection().commit();  // JTAデータソースには必要ありません
sess.close();

独自にコネクションを用意すれば、close() はそれへの参照を返すので、 手作業でクローズしてプールに戻すことができます。 さもなければ close() はそれをプールに戻します。

9.8. 例外処理

Hibernateを使っていると例外に出くわすことがあります。 それは大抵、チェックされた HibernateException です。 この例外は、入れ子状の根本原因(root cause)を持つことができます。 それにアクセスするには、getCause() メソッドを使ってください。

Session が例外をスローした場合は、 即座にトランザクションをロールバックし、Session.close() をコールし、Session インスタンスを捨ててください。 Session のメソッドの中には、 Sessionを整合性のある状態に しない ものがあります。 つまりこれは、Hibernateがスローする例外は全て致命的なものであるということです。 そのため、チェックされた HibernateExceptionRuntimeException に変換したい場合があるかもしれません (最も簡単なのは、HibernateException.javaextends を置き換えて、Hibernateを再コンパイルする方法です)。 チェック例外はHibernateの遺物であることに注意してください。 これはHibernateの将来のメジャーバージョンで変更されます。

データベースとのやりとりの最中にスローされたSQLExceptionは、 HibernateによってJDBCExceptionのサブクラスへの変換が試みられます。 根本原因のSQLExceptionには、JDBCException.getCause() をコールすることでアクセスできます。 HibernateによるSQLExceptionから適切なJDBCExceptionのサブクラスへの変換は、 SessionFactoryに関連付けられたSQLExceptionConverterに基づいて行われます。 デフォルトでは、どのSQLExceptionConverterが使われるかは、 設定されたデータベースの方言によって決まります。 しかし、カスタムの実装をプラグすることも可能です (詳細については、SQLExceptionConverterFactoryクラスのjavadocを見てください)。 標準のJDBCExceptionのサブタイプは、以下のものです:

  • JDBCConnectionException - ベースとなる JDBCコミュニケーションに関するエラーを示唆します。

  • SQLGrammarException - 発行したSQLに関する 文法や構文の問題を示唆します。

  • ConstraintViolationException - 何らかの 整合性制約違反を示唆します。

  • LockAcquisitionException - リクエストされた操作の実行に 必要なロックレベルの取得に関するエラーを示唆します。

  • GenericJDBCException - 他のカテゴリに該当しない 一般的な例外です。

常に全ての例外は、現時点の Session とトランザクションに対して致命的であると考えられます。 HibernateがさまざまなSQLExceptionの型を区別できるようになったからといって、 Sessionに対してこれらの例外が復帰可能なものになったわけではありません。 この例外の型階層によって、例外の原因のカテゴリに対する プログラムの反応を容易にしただけです。

以下の例外処理イディオムは、Hibernateを使うアプリケーションでの典型例です:

Session sess = factory.openSession();
Transaction tx = null;
try {
    tx = sess.beginTransaction();
    // do some work
    ...
    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    throw e;
}
finally {
    sess.close();
}

また、JDBCトランザクションを自分で管理するときは:

Session sess = factory.openSession();
try {
    // do some work
    ...
    sess.flush();
    sess.connection().commit();
}
catch (Exception e) {
    sess.connection().rollback();
    throw e;
}
finally {
    sess.close();
}

また、JTAに登録されたデータソースを使うときは:

SessionContext ctx = ... ;
Session sess = factory.openSession();
try {
    // do some work
    ...
    sess.flush();
}
catch (Exception e) {
    // ctx.setRollbackOnly();
    throw new EJBException(e);
}
finally {
    sess.close();
}

(JTAで管理される環境における)アプリケーション・サーバは、 java.lang.RuntimeException に対してのみ、 トランザクションを自動ロールバックするということを心に留めておいてください。 アプリケーションの例外(つまり HibernateException )が発生した場合は、 EJBContext において setRollbackOnly() を自分でコールしなければいけません。 もしくは上の例のように、自動ロールバックするために RuntimeException (例えば、EJBException )でラップしなければいけません。

9.9. ライフサイクルとオブジェクトのグラフ

関連オブジェクトのグラフの中のすべてのオブジェクトをセーブまたは更新するためには、 以下のどちらかを行わなければなりません

  • 個々のオブジェクトに対して save() または saveOrUpdate() または update() するか

  • cascade="all" または cascade="save-update" を使い、 関連オブジェクトをマッピングする。

同じように、グラフのすべてのオブジェクトを削除するには、以下のどちらかを行います

  • 個々のオブジェクトを delete() するか

  • cascade="all" または cascade="all-delete-orphan" または cascade="delete" を使い、関連オブジェクトをマッピングする。

おすすめ:

  • もし子オブジェクトの生存期間が親オブジェクトの生存期間に制限を受けるなら、 cascade="all" と指定して、 それを ライフサイクル・オブジェクトにしてください。

  • さもなければ明示的にそれを save() し、 そしてアプリケーション・コードから delete() してください。 もし本当に余分なタイピングをしてセーブを行いたければ、 cascade="save-update" を使い、 明示的に delete() してください。

cascade="all" と関連をマッピングすると(many-to-oneまたはコレクション)、 その関連は 親 / 子 スタイルの関係としてマークされます。 それは親のセーブ/更新/削除を行うと、子のセーブ/更新/削除が行われる関係です。 さらに永続的な親から子への単なる参照すると、子のセーブ/更新が行われます。 しかしメタファーは不完全です。 cascade="all-delete-orphan" でマッピングされた <one-to-many> 関連の場合を除いて、 子が親から参照されなくなっても自動的には削除 されません。 操作のカスケードの正確な意味は以下のようになります:

  • もし親がセーブされれば、すべての子は saveOrUpdate() に渡される

  • もし親が update() または saveOrUpdate() に渡されれば、 すべての子は saveOrUpdate() に渡される

  • もし一時的な子が永続的な親から参照されるようになれば、 子は saveOrUpdate() に渡される

  • もし親が削除されれば、すべての子は delete() に渡される

  • もし一時的な子が永続的な親から参照されなくなっても、 特別には何も起こらない。 (必要ならアプリケーションで明示的に子を削除すべきです) ただし cascade="all-delete-orphan" の場合は除きます。 この場合は「みなしご」は削除されます。

Hibernateは「到達可能性による永続化」を完全には実装していません。 それは(非効率的な)永続ガベージ・コレクションを含意するからです。 しかし一般的な要求のため、Hibernateは他の永続オブジェクトから参照されたときに、 エンティティが永続化されるようになる考えをサポートしています。 cascade="save-update" とマークされた関連はこのように振る舞います。 もしアプリケーションを通してこの方法を使いたければ、 単純に <hibernate-mapping> 要素の default-cascade 属性を指定するだけです。

9.10. インターセプタ

Interceptor インターフェイスはSessionからアプリケーションへの コールバックを用意しています。 それはセーブ、更新、削除、ロードの前に、 永続オブジェクトのプロパティを見たり操作したりできます。 これの使いみちで考えられるものは、コード監査のための情報を追跡することです。 例えば以下の Interceptor は、 Auditable が作成されたとき createTimestamp> を自動的に設定し、 Auditable が更新されたとき lastUpdateTimestamp プロパティを更新します。

package net.sf.hibernate.test;

import java.io.Serializable;
import java.util.Date;
import java.util.Iterator;

import net.sf.hibernate.Interceptor;
import net.sf.hibernate.type.Type;

public class AuditInterceptor implements Interceptor, Serializable {

    private int updates;
    private int creates;

    public void onDelete(Object entity,
                         Serializable id,
                         Object[] state,
                         String[] propertyNames,
                         Type[] types) {
        // 何もしません
    }

    public boolean onFlushDirty(Object entity, 
                                Serializable id, 
                                Object[] currentState,
                                Object[] previousState,
                                String[] propertyNames,
                                Type[] types) {

        if ( entity instanceof Auditable ) {
            updates++;
            for ( int i=0; i < propertyNames.length; i++ ) {
                if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
                    currentState[i] = new Date();
                    return true;
                }
            }
        }
        return false;
    }

    public boolean onLoad(Object entity, 
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types) {
        return false;
    }

    public boolean onSave(Object entity,
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types) {
        
        if ( entity instanceof Auditable ) {
            creates++;
            for ( int i=0; i<propertyNames.length; i++ ) {
                if ( "createTimestamp".equals( propertyNames[i] ) ) {
                    state[i] = new Date();
                    return true;
                }
            }
        }
        return false;
    }

    public void postFlush(Iterator entities) {
        System.out.println("Creations: " + creates + ", Updates: " + updates);
    }

    public void preFlush(Iterator entities) {
        updates=0;
        creates=0;
    }
    
    ......
    ......
    
}

インターセプタはSessionを作成するときに指定します。

Session session = sf.openSession( new AuditInterceptor() );

また、Configuration を使い、グローバルレベルでインターセプタを設定することもできます:

new Configuration().setInterceptor( new AuditInterceptor() );

9.11. メタデータAPI

Hibernateはすべてのエンティティとバリューの型の非常にリッチなメタレベル・モデルを必要とします。 このモデルがアプリケーション自体にとってとても役に立つことが時折あります。 例えばアプリケーションが「賢い」ディープ・コピーアルゴリズムを実装するために、 Hibernateのメタデータを使うかもしれません。 それはどのオブジェクトをコピーすべきか(例 更新可能なバリュー型)、 またはすべきでないか(例 更新不能バリュー型、可能なら関連エンティティ)を 理解するために必要です。

Hibernateは ClassMetadataCollectionMetadata インターフェイスと Type 階層を通して、メタデータを公開しています。 メタデータ・インターフェイスのインスタンスは SessionFactory から取得することができます。

Cat fritz = ......;
Long id = (Long) catMeta.getIdentifier(fritz);
ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
Object[] propertyValues = catMeta.getPropertyValues(fritz);
String[] propertyNames = catMeta.getPropertyNames();
Type[] propertyTypes = catMeta.getPropertyTypes();
// コレクションや関連ではない、すべてのプロパティのマップを取得します
// TODO: コンポーネントの何?
Map namedValues = new HashMap();
for ( int i=0; i<propertyNames.length; i++ ) {
    if ( !propertyTypes[i].isEntityType() && !propertyTypes[i].isCollectionType() ) {
        namedValues.put( propertyNames[i], propertyValues[i] );
    }
}