Hibernate 入門記 永続クラス

今回は「Chapter 4. Persistent Classes」へ進みます.
まずは「4.1. A simple POJO example」です.ここで Cat という永続クラスの例が出てくるのですが,

Most Java applications require a persistent class representing felines.

とのことです.felinesはネコ科の動物のことらしいんですが(他にも「ずるい」とかいう意味もあるらしい),多くのJavaアプリケーションが必要とするって?(^^;
そんなことはともかく,ここはHibernateで扱うことのできる永続クラスが備えるべき要件が解説されています.
まずは

  • アクセッサ(getter)およびミューテータ(setter)を持つこと.

ようするにプロパティですね.ORマッピングツールによっては永続クラスのフィールドを永続化するものもあるとのことですが(ODBMSの多くはそうだったような),Hibernateは実装の詳細と永続メカニズムを切り離したほうがいいと考えているとのこと.そこで,setter/getterを通して永続クラスの状態にアクセスする,と.
そのgetter/setterのメソッドですが,public でなくてもよいとのこと.private でもいいらしい.へぇ〜.どっちみち中ではリフレクション使いまくりなのでしょうから,構わないっていえばそうなんでしょうけど.
ということで「入門寸前」のCustomerクラスのgetter/setterメソッドを全てprivateにしてみましたが,うん,確かにすんなりと動きました.
それから次に,

  • デフォルトコンストラクタを持つこと.

Hibernateは永続クラスのインスタンスを生成するのにConstructor#newInstance()を使うとのことで,引数のないコンストラクタが必要になるとのこと.これもまた public でなくてもいいとのことです.
ということで「入門寸前」のCustomerクラスに private のデフォルトコンストラクタを追加してみましたが,うん,確かにすんなりと動きました.
ついでにデフォルトコンストラクタをなくしてみたところ,

net.sf.hibernate.PropertyNotFoundException: Object class study.Customer must declare a default (no-argument) constructor

と怒られました.てへっ.
必須の要件はこの2つだけみたいです.要するに普通のJavaBeansならOKってことですね.
他にオプションの要件として,

  • IDプロパティを提供すること.

というのがあります.IDプロパティというのは,対応するテーブルの主キーの値を持つプロパティとのこと.永続クラスは,一貫した名前のIDプロパティをもつことを推奨するとのことです.らじゃ.
この用件はオプションなので,なくてもいいわけですが,その場合はカスケードされた更新とか,updateinsert かをHibernateが自動的に判断してくれる機能が使えないなどの制限があるとのこと.
これって,あくまでも永続クラスには主キーの値を保持するプロパティがなくてもいい,ということであって,テーブルに主キーがなくてもいいというわけではなさそうなので注意.一瞬,「あれ? この前できないと思ったOracleのDUAL表も扱えるのかな?」と思ったのですが,ちょっと違うみたいです.
それからもう一つ,ちょっと気になる記述が.

If your legacy database table has composite keys, you can even use a user-defined class with properties of these types

えぇぇぇっ? 複合キーってレガシーなんですか? モダンなデータベースでは複合キーは使わないんですか? うそぉ?
うーみゅ,いつの間にそんな時代になっていたんだろう? っていうか,オブジェクトIDの勝利ですか? なんか違うっぽい...
ともかく複合キーも使えないわけではないぞ,と.
さらに気になる記述が.IDプロパティはプリミティブ型ではなく,null にすることのできる型を推奨とのことです.なぜ? 主キーが NULL になることはないはずなので,プリミティブ型でも構わないのでは? うーみゅ,推奨するとは書いてあるものの,なぜ推奨するのかが書かれていない... 無念だ.遠くない未来に解説があることに期待しよう.
それから次のオプション要件.

  • final のクラスであること

おぉっと,ここで前回驚かされたCGLIBに絡んでいそうな説明がでてきます.
なんかですね,パフォーマンスのためにProxyを使うみたいなことが書いてあります.final なクラスでも interface を実装したクラスのどちらでもないクラスでも,永続化はできるけどパフォーマンスチューニングには制限されちゃうよ,とのこと.うひゃー,そんなことまでやってるんだぁ.あなどれん...
そのチューニングの話は「Chapter 14. Improving performance」に解説されているようです.14章か... 遠いな...
ともあれ(JW),「入門寸前」のCustomerクラスを final にしてみましたが,うん,確かにすんなりと動きました.


ということで次は「4.2. Implementing inheritance」です.
...
うぬぬ,書いてあることは簡単なんだけど...

A subclass must also observe the first and second rules. It inherits its identifier property from Cat.

これだけですよ,これだけ.この後に派生クラスの例が出てるんですが...
派生クラスでも,先の必須の要件を満たさないといけないってのは分かります.その次なんですが,これは単なる例の話なんでしょうか? それとも must なんでしょうか? 今のところ,永続クラスの継承がどのようにテーブルにマップされるとか分からないので,ちょっと判断がつきません.一応これも必須の要件と仮定して,

  • 派生クラスのIDプロパティは基底クラスから継承すること

を忘れないようにしよう.


どんどん進んで次は「4.3. Implementing equals() and hashCode()」です.
ほほぅ,翻訳(2.0.x)の「1.2. 永続オブジェクトの同一性 (Identity)」が2.1.xでなくなったのは,ここに来たわけですか.ちょっと内容も変わってるぅ.
えっとですね,Hibernateでは,異なった Session で同じテーブルの同じ行にアクセスした場合,異なったインスタンスが返ってくるらしいです.そりゃそうでしょう,トランザクションも異なるのが普通だろうし.
ということは,そのままだと equals(Object) メソッドも false になってしまいます.これはもしかしたら嬉しくないかも.とくに永続オブジェクトをWEBコンテナのセッションに保存したりして,その上で同一性を判定したい場合とか.
そういう場合は,永続クラスの equals(Object) をオーバーライドしなさいってことですね.当然その場合は hashcode() のオーバーライドも忘れずに,と.
なんですが,単純にIDプロパティの同値性を判断してもいけないみたい.というのもですね,永続クラスの新しいインスタンスが生成された時点では,IDプロパティの値は設定されないらしいんですね.あう,だからIDプロパティは null にできる型を推奨なのでしょうか? そんな気がする.
そんなわけで,結局のところは「ビジネスキー」の同値性を使って判定しろとのことです... つまりHibernateを使う際には,主キーはHibernate用に INTEGER とかで無意味なIDにしておいて,業務的なキーは主キーにするなってことですか? まぁ,主キーは候補キーの一つに過ぎないというのも事実な訳ですが... DB屋さんに反発食らわないかなぁ?(^^;


難しい話は忘れて,次は「4.4. Lifecycle Callbacks」です.やっぱりプログラミングネタが気楽でいいネ!
永続クラスは,DBからロードされたり保存されたりした場合のイベントをHibernateから受け取ることができます.そのためには,

  • Lifecycle

という interfaceimplements します.で,こいつには

    • boolean onSave(Session)
    • boolean onUpdate(Session)
    • boolean onDelete(Session)
    • void onLoad(Session, Serializable)

というメソッドがあります.
onSave(Session)Session#save(Object) などが呼び出されたときにコールバックされるみたい.Session#save(Object) っていつ使うの? って思ったら,これはDBへのINSERTですか.これはIDの割り当てが行われた後に呼び出される(そうでない場合もあるみたい)とのことですが,主キーの割り当てそのものの理解がまだなのであった.無念だ.
で,onUpdate(Session) は,Session#update(Object) などが呼び出された直後にコールバックされるもので,こちらはDBへのUPDATE,同じくonDelete(Session)Session#delete(Object) などが呼び出された直後にコールバックされるもので,DBへのDELETE,と.ふむふむ.
これらのメソッドは,true を返すことで更新を拒否することができるそうです.その結果,更新しようとしたアプリには何がどうなるのでしょうか? silently vetoed とあるので,何も通知されないのかな?
CallbackException をスローしてもよくて,その場合はアプリまで吹っ飛んでいくとのことです.
最後の onLoad(Session, Serializable) は,DBから読み込まれた後にコールされるみたいですが,キャッシュとかの影響は? ちょっとよく分からない...
うーみゅ,Hibernate全体の理解が足りないためか,謎が多いです.いつかまた戻ってこなくてはいけないかも.


もうひとつ,「4.5. Validatable callback」によると,永続オブジェクトは永続化される前に妥当性を検証することができます.そのためには,

  • Validatable

という interfaceimplements します.これには,

    • void validate()

というメソッドがあります.妥当でない場合は ValidationFailure をスローしろとのこと.
なのですが,どういうわけだかこのコールバックメソッド,呼び出されるタイミングは予期できないとのこと.なので,業務的な機能の実現には使うなって... どういうこと? いいや,こういうのは忘れましょう.


あと,XDocletを使った永続クラスの例が出てくるのですが,ソースだけで解説がないので無視します.心より恥じる.
ということで,実験コーナー!! 今回は当然,Lifecycle に決まっています.そのためには永続オブジェクトを生成したり更新したり削除したりしなきゃいけないわけですか.まだやったことないぞ.頑張るべし.
ということで,まずは永続クラスで Lifecycleimplements しましょう.
こんな感じ.

public final class Customer implements Lifecycle {
    public void onLoad(Session s, Serializable id) {
        System.out.println("onLoad() : " + toString());
    }
    public boolean onSave(Session s) throws CallbackException {
        System.out.println("onSave() : " + toString());
        return false;
    }
    public boolean onUpdate(Session s) throws CallbackException {
        System.out.println("onUpdate() : " + toString());
        return true;
    }
    public boolean onDelete(Session s) throws CallbackException {
        System.out.println("onDelete() : " + toString());
        throw new CallbackException(toString());
    }
    //後は「入門寸前」のソースと同じ

onSave() は成功,onUpdate()は静かに失敗,onDelete()は派手に失敗する! はず!
マッピングファイルや構成定義のファイル,それにテーブル(customer)は「入門寸前」と同じです.でもデータは入れていません(空のテーブル).
そして実行用のクラス.

package study;
import java.util.Iterator;
import java.util.List;

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

public class Main {
    public static void main(String[] args) {
        try {
            Configuration config = new Configuration();
            SessionFactory factory = config.configure().buildSessionFactory();
            Session session = factory.openSession();

            Customer yuri = new Customer();
            yuri.setId(1);
            yuri.setFirstName("Yuri");
            yuri.setLastName("Ebihara");
            session.save(yuri);
            session.flush();

            Customer akiko = new Customer();
            akiko.setId(2);
            akiko.setFirstName("Akiko");
            akiko.setLastName("Yada");
            session.save(akiko);
            session.flush();

            akiko.setLastName("Wada");
            session.update(akiko);
            session.flush();

            List customers = session.find("from Customer");
            Iterator it = customers.iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }

            session.delete(yuri);
            session.delete(akiko);
            session.flush();
        }
        catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

id:fumi0611さんのリクエストに答えてみました.(^^;
単なるネタ切れなのは内緒です.
こいつを実行!!!

 onSave() : 1 : Yuri Ebihara, null null
 Hibernate: insert into customer (firstName, lastName, street, city, id) values (?, ?, ?, ?, ?)
 onSave() : 2 : Akiko Yada, null null
 Hibernate: insert into customer (firstName, lastName, street, city, id) values (?, ?, ?, ?, ?)
 Hibernate: update customer set firstName=?, lastName=?, street=?, city=? where id=?
 Hibernate: select customer0_.id as id, customer0_.firstName as firstName, customer0_.lastName as lastName, customer0_.street as street, customer0_.city as city from customer customer0_
 1 : Yuri Ebihara, null null
 2 : Akiko Wada, null null
 onDelete() : 1 : Yuri Ebihara, null null
 net.sf.hibernate.CallbackException: 1 : Yuri Ebihara, null null
     at study.Customer.onDelete(Customer.java:64)
     at net.sf.hibernate.impl.SessionImpl.doDelete(SessionImpl.java:1202)
     at net.sf.hibernate.impl.SessionImpl.delete(SessionImpl.java:1165)
     at study.Main.main(Main.java:40)

ぐはぁっ,矢田亜希子和田アキ子になってしまった!?
なぜ? どうして Session#update(Object) が成功しているの? わけわからず Session#flush() 使ってるから?
うー,なぜか分からないのですが,今日はこれからオレコンなので時間切れです... 無念だ.


05/22 13:30 追記
Lifecycle#onUpdate(Session) は,トランジェントな(永続化されていない)オブジェクトが Session#update(Object) に渡された場合にのみ,コールバックされるとのことです.今回のサンプルでは,すでに永続化したオブジェクトのプロパティを値を変更して Session#update(Object) を呼び出していたため,Lifecycle#onUpdate(Session) は呼び出されなかったということのようです.
さっそく,トランジェントなオブジェクトを作って試してみたところ,Session#update(Object) を呼び出してもINSERTされることが分かりました.UPDATEじゃないのですね.ということで,今回のように矢田亜希子和田アキ子に変更する(そして失敗する)というような用途では,どうやっても Lifecycle#onUpdate(Session) はコールバックされないようです.うーみゅ.
まだまだHibernateの基本的なところが分かっていないということを痛感します.っていうか分かっていないからこそ入門しているんですけどね.無念だ.