第4章 永続クラス

永続クラスは、ビジネスの問題のエンティティを実装する、アプリケーションのクラスです (例 Eコマース・アプリケーションの顧客や注文)。 名前からわかる通り、永続クラスは一時的なインスタンスと、 データベースに格納される永続インスタンスを含んでいます。

このクラスがいくつかの簡単なルールを守れば、Hibernateは最も良く動作します。 これはプレーンの古いJavaオブジェクト(POJO)プログラミング・モデルとしても知られています。

4.1. 簡単なPOJOの例

ほとんどのJavaアプリケーションには、ネコ科を表す永続クラスが必要です。

package eg;
import java.util.Set;
import java.util.Date;

public class Cat {
    private Long id; // 識別子
    private String name;
    private Date birthdate;
    private Cat mate;
    private Set kittens
    private Color color;
    private char sex;
    private float weight;

    private void setId(Long id) {
        this.id=id;
    }
    public Long getId() {
        return id;
    }

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

    void setMate(Cat mate) {
        this.mate = mate;
    }
    public Cat getMate() {
        return mate;
    }

    void setBirthdate(Date date) {
        birthdate = date;
    }
    public Date getBirthdate() {
        return birthdate;
    }
    void setWeight(float weight) {
        this.weight = weight;
    }
    public float getWeight() {
        return weight;
    }

    public Color getColor() {
        return color;
    }
    void setColor(Color color) {
        this.color = color;
    }
    void setKittens(Set kittens) {
        this.kittens = kittens;
    }
    public Set getKittens() {
        return kittens;
    }
    // addKittenは、Hibernateにとっては必要ありません
    public void addKitten(Cat kitten) {
        kittens.add(kitten);
    }
    void setSex(char sex) {
        this.sex=sex;
    }
    public char getSex() {
        return sex;
    }
}

以下に守るべき4つのルールがあります:

4.1.1. 永続フィールドに対するアクセサとミューテータを定義する

Cat はすべての永続フィールドに対する アクセサ・メソッドを定義しています。 他の多くのORMツールはインスタンス変数を直接永続化します。 私たちは永続化機構から実装の詳細を分離する方がずっと良いことだと信じています。 HibernateはJavaBeansスタイルのプロパティを永続化し、 getFoo, isFoo, setFoo のような形式のメソッド名を認識します。

プロパティをパブリックで定義する必要は ありません。 Hibernateはデフォルト, protected, private のget / setのペアを持つプロパティを永続化できます。

4.1.2. デフォルト・コンストラクタを実装する

Cat には暗黙的なデフォルト(引数なし)コンストラクタがあります。 すべての永続クラスはデフォルト・コンストラクタ(パブリックでなくても構いません) を持たなければいけません。 そのためHibernateは Constructor.newInstance() を使って、 それらをインスタンス化できます。

4.1.3. 識別子プロパティを用意する(オプション)

Cat には id というプロパティがあります。 プロパティはどのような名前でも構いませんし、 型もどんなプリミティブ型でも、プリミティブの「ラッパー」型でも、 java.lang.Stringjava.util.Date 型でも構いません。 (レガシー・データベースのテーブルに複合キーがあれば、 これらのプロパティを持つユーザ定義クラスを使うこともできます。 以下の複合識別子の節を見てください。)

識別子プロパティはオプションです。 これを使わずに、Hibernateが内部的にオブジェクトの識別子を追いかけるようにもできます。 しかし識別子を使うことが、多くのアプリケーションにとって良い(そしてとても一般的な) 設計の決定となります。

さらにいくつかの機能は、識別子プロパティを定義するクラスだけで利用することができます:

  • カスケード更新(「ライフサイクル・オブジェクト」を見てください)

  • Session.saveOrUpdate()

すべての永続クラスで一貫した名前の識別子プロパティを定義することをおすすめします。 さらにnullでない(つまりプリミティブ型でない)型を使うことをおすすめします。

4.1.4. finalクラスにしない(オプション)

Hibernateの核となる機能の プロキシ は finalではない永続クラスに依存するか、 またはすべてのパブリック・メソッドを定義するインターフェイスに依存しなければなりません。

Hibernateはインターフェイスを実装しない final クラスを永続化できますが、 プロキシを使うことはできません。 それはパフォーマンス・チューニングのオプションをいくらか制限します。

4.2. 継承の実装

サブクラスは1番目と2番目のルールに従わなければいけません。 それは識別子プロパティを Cat から継承します。

package eg;

public class DomesticCat extends Cat {
        private String name;

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

4.3. equals()hashCode() の実装

永続クラスの混合オブジェクト(例えば Set で)を使うつもりなら、 equals()hashCode() をオーバーライドしなければいけません。

これは2つの異なる Session で、 これらのオブジェクトがロードされるときだけにあてはまります。 というのはHibernateは1つの Session でJVMアイデンティティ ( a == b , equals() のデフォルトの実装) だけを保証するからです。

ab の両方のオブジェクトが、 同じデータベースの行だったとしても(識別子として同じ主キーを持つ)、 ある特定の Session コンテキストの外で それらが同じJavaインスタンスであることを保証できません。

最も明快な方法は、両方のオブジェクトの識別子の値の比較に、 equals() / hashCode() を実装する方法です。 その値が同じなら両者は同じデータベースの行でなければならず、 そのためそれらは等しくなります (もし両者が Set に加えられれば、 Set に1つの要素だけを持つことになります)。 あいにく私たちはこの方法を使うことができません。 Hibernateは永続であるオブジェクトだけに識別子の値を割り当て、 新しく作成されたインスタンスは識別子の値を持ちません。 私たちは ビジネス・キー等価性 を使い、 equals()hashCode() を実装することをおすすめします。

ビジネス・キー等価性とは、 equals() メソッドがビジネス・キーを形作るプロパティだけを比較することを意味します。 キーは現実世界の私たちのインスタンスを識別します (自然な 候補キーです):

public class Cat {

    ...
    public boolean equals(Object other) {
        if (this == other) return true;
        if (!(other instanceof Cat)) return false;

        final Cat cat = (Cat) other;

        if (!getName().equals(cat.getName())) return false;
        if (!getBirthday().equals(cat.getBirthday())) return false;

        return true;
    }

    public int hashCode() {
        int result;
        result = getName().hashCode();
        result = 29 * result + getBirthday().hashCode();
        return result;
    }

}

候補キー(この例では名前と誕生日の複合物)は ある特定の比較操作だけに妥当であることを心に留めておいてください (おそらく1つのユースケースだけです)。 普通は現実の主キーに当てはまる、安定criteriaは必要ありません。

4.4. Lifecycleコールバック

オプションとして永続クラスは Lifecycle インターフェイスを実装するかもしれません。 それは永続オブジェクトがセーブやロードの後、そして削除や更新の前に、 必要な初期化/クリーンアップを実行することを可能にするコールバックを提供します。

しかしHibernateはでしゃばりすぎないもう一つの Interceptor も用意しています。

public interface Lifecycle {
        public boolean onSave(Session s) throws CallbackException;   (1)
        public boolean onUpdate(Session s) throws CallbackException; (2)
        public boolean onDelete(Session s) throws CallbackException; (3)
        public void onLoad(Session s, Serializable id);              (4)
}
(1)

onSave - オブジェクトがセーブまたは挿入される直前に コールされます

(2)

onUpdate - オブジェクトが更新される直前 (オブジェクトが Session.update() に渡されるとき) にコールされます

(3)

onDelete - オブジェクトが削除される直前にコールされます

(4)

onLoad - オブジェクトがロードされた直後にコールされます

onSave(), onDelete(), onUpdate() は依存オブジェクトのカスケード・セーブと カスケード削除のために使うことができます。 これはマッピング・ファイルでカスケード操作を定義する代わりの方法となります。 onLoad() は永続状態から オブジェクトの一時的プロパティを初期化することに使うことができます。 しかし Session インターフェイスは このメソッドの内部から起動されないかもしれないので、 それを依存オブジェクトのロードのために使うことはできません。 他に考えられる onLoad(), onSave(), onUpdate() の使い方は、 後で使うために現在の Session への参照を保存しておくことです。

onUpdate() はオブジェクトの永続状態が更新されるたびに 必ずコールされるわけではないことに注意してください。 それがコールされるのは、一時的オブジェクトが Session.update() に渡されるときだけです。

もし onSave(), onUpdate(), onDelete()true を返すなら、 その操作は中止されます。 CallbackException がスローされれば、 その操作は中止され、アプリケーションに例外が返されます。

onSave() はnativeキー生成が使われるときを除いて、 オブジェクトに識別子が割り当てられた後にコールされることに注意してください。

4.5. Validatableコールバック

永続クラスが状態を永続化する前に不変式をチェックする必要があれば、 以下のインターフェイスを実装することができます:

public interface Validatable {
        public void validate() throws ValidationFailure;
}

不変式が満たされなければ、オブジェクトは ValidationFailure をスローすべきです。 Validatable のインスタンスは、 validate() の内部で状態を更新すべきではありません。

Lifecycle インターフェイスのコールバック・メソッドとは違い、 validate() は何回コールされるかわかりません。 アプリケーションはビジネスの機能を validate() に依存させるべきではありません。

4.6. XDocletマークアップの使用

次の章では簡単で読みやすいXMLフォーマットを使い、 Hibernateのマッピングがどのように表現されるかをお見せします。 Hibernateユーザの多くはXDocletの @hibernate.tags を使い、 マッピング情報を直接ソースコードに埋め込む方法を好みます。 厳密にはこれはXDocletの領分だと考えられるので、 この方法についてはこのドキュメントでは述べません。 しかしXDocletマッピングを使った Cat の例を以下にお見せします。

package eg;
import java.util.Set;
import java.util.Date;

/**
 * @hibernate.class
 *  table="CATS"
 */
public class Cat {
    private Long id; // 識別子
    private Date birthdate;
    private Cat mate;
    private Set kittens
    private Color color;
    private char sex;
    private float weight;

    /**
     * @hibernate.id
     *  generator-class="native"
     *  column="CAT_ID"
     */
    public Long getId() {
        return id;
    }
    private void setId(Long id) {
        this.id=id;
    }

    /**
     * @hibernate.many-to-one
     *  column="MATE_ID"
     */
    public Cat getMate() {
        return mate;
    }
    void setMate(Cat mate) {
        this.mate = mate;
    }

    /**
     * @hibernate.property
     *  column="BIRTH_DATE"
     */
    public Date getBirthdate() {
        return birthdate;
    }
    void setBirthdate(Date date) {
        birthdate = date;
    }
    /**
     * @hibernate.property
     *  column="WEIGHT"
     */
    public float getWeight() {
        return weight;
    }
    void setWeight(float weight) {
        this.weight = weight;
    }

    /**
     * @hibernate.property
     *  column="COLOR"
     *  not-null="true"
     */
    public Color getColor() {
        return color;
    }
    void setColor(Color color) {
        this.color = color;
    }
    /**
     * @hibernate.set
     *  lazy="true"
     *  order-by="BIRTH_DATE"
     * @hibernate.collection-key
     *  column="PARENT_ID"
     * @hibernate.collection-one-to-many
     */
    public Set getKittens() {
        return kittens;
    }
    void setKittens(Set kittens) {
        this.kittens = kittens;
    }
    // addKittenは、Hibernateにとっては必要ありません
    public void addKitten(Cat kitten) {
        kittens.add(kitten);
    }

    /**
     * @hibernate.property
     *  column="SEX"
     *  not-null="true"
     *  update="false"
     */
    public char getSex() {
        return sex;
    }
    void setSex(char sex) {
        this.sex=sex;
    }
}