第4章 永続クラス

永続クラスはビジネス上の問題のエンティティ(例えば、Eコマースアプリケーションの顧客や注文) を実装するアプリケーションのクラスです。 永続クラスのすべてのインスタンスが永続状態であると見なされるわけではありません。 インスタンスは逆に一時的(transient)であったり、分離状態(detached)であったりするかもしれません。

Plain Old Java Object (POJO)プログラミングモデルとしても知られる いくつかの単純なルールに従うなら、Hibernateは最もよく働きます。 しかしこれらのルールは難しいものではありません。 実際Hibernate3は永続オブジェクトの性質にほとんど何の前提も置いていません。 ドメインモデルは他の方法で表現することもできます。 例えば Map インスタンスのツリーを使う方法があります。

4.1. 単純なPOJOの例

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

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

public class Cat {
    private Long id; // identifier

    private Date birthdate;
    private Color color;
    private char sex;
    private float weight;
    private int litterId;

    private Cat mother;
    private Set kittens = new HashSet();

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

    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 setSex(char sex) {
        this.sex=sex;
    }
    public char getSex() {
        return sex;
    }

    void setLitterId(int id) {
        this.litterId = id;
    }
    public int getLitterId() {
        return litterId;
    }

    void setMother(Cat mother) {
        this.mother = mother;
    }
    public Cat getMother() {
        return mother;
    }
    void setKittens(Set kittens) {
        this.kittens = kittens;
    }
    public Set getKittens() {
        return kittens;
    }
    
    // addKitten not needed by Hibernate
    public void addKitten(Cat kitten) {
        kitten.setMother(this);
        kitten.setLitterId( kittens.size() ); 
        kittens.add(kitten);
    }
}

従うべき4つのルールがあります:

4.1.1. 引数のないコンストラクタを実装する

Cat には引数のないコンストラクタがあります。 Hibernateが Constructor.newInstance() を使って永続クラスの インスタンス化を行えるように、すべての永続クラスにはデフォルトコンストラクタ (publicでなくても構いません)がなければなりません。 Hibernateの実行時プロキシ生成のために、少なくとも package の可視性を持つデフォルトコンストラクタを強くお勧めします。

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

Cat には id というプロパティがあります。 このプロパティはデータベーステーブルの主キーカラムへマッピングされます。 このプロパティの名前は何でも構いませんし、型はどのようなプリミティブ型でも、 プリミティブの「ラッパー」型でも、java.lang.Stringjava.util.Date でも構いません。 (もしレガシーデータベーステーブルが複合キーを持つならば、 今述べたような型のプロパティを持つユーザ定義のクラスを使うことさえ可能です。 後で複合識別子の節を見てください。)

識別子プロパティは厳密にはオプションです。 これを省略して、Hibernateに内部的にオブジェクトの識別子を追跡させることは可能です。 しかしおすすめはしません。

実際に、識別子プロパティを宣言するクラスだけが利用可能な機能がいくつかあります:

  • 分離オブジェクトの連鎖的な再追加(カスケード更新やカスケードマージ)。 項10.11. 「連鎖的な永続化」 を参照してください。

  • Session.saveOrUpdate()

  • Session.merge()

永続クラスには、一貫した名前の識別子プロパティを定義することをお勧めします。 さらにnull値を取れる(つまりプリミティブではない)型を使った方がよいでしょう。

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

Hibernateの中心的な特徴である プロキシ は、 永続クラスがfinalでないこと、またはメソッドを全部publicで宣言している インターフェイスが実装されているかに依存しています。

Hibernateでインターフェイスを実装していない final クラスを永続化することはできますが、 遅延関連フェッチに対してプロキシを使うことはできなくなります。 これはパフォーマンスチューニングへの選択肢を狭めることになります。

finalではないクラスで public final メソッドを定義することも避けるべきです。 public final メソッドを持つクラスを使いたければ、 lazy="false" と設定して明示的にプロキシを無効にしなければなりません。

4.1.4. 永続フィールドに対するアクセサとミューテータを定義する(オプション)

Cat ではすべての永続フィールドに対してアクセサメソッドを定義しています。 他の多くのORMツールは、永続インスタンス変数を直接永続化します。 私たちはリレーショナルスキーマとクラスの内部構造を分離する方が良いと信じています。 デフォルトでは、HibernateはJavaBeanスタイルのプロパティを永続化し、 getFoo, isFoo, setFoo 形式のメソッド名を認識します。 しかし必要なら、特定のプロパティに対して、直接のフィールドアクセスに切り替えることは可能です。

プロパティはpublicで宣言する必要は ありません 。 Hibernateはデフォルト、protected もしくは private のget / setのペアを持つプロパティを永続化することができます。

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() の実装

以下の条件の場合、 equals()hashCode() メソッドをオーバーライドしなければなりません、

  • 永続クラスのインスタンスを Set に置く場合。 (これは多値の関連を表現するおすすめの方法です) そして同時に

  • 分離インスタンスをセッションへ再追加する場合。

Hibernateは、永続ID(データベースの行)と、特定のセッションスコープ内に 限定ですがJavaIDとが等価であることを保証します。 ですから異なるセッションで検索したインスタンスを組み合わせる場合、 Set に意味のあるセマンティクスを持たせようと思っているなら すぐにequals()hashCode() を実装しなければなりません。

最も明白な方法は、両方のオブジェクトの識別子の値の比較によって equals()hashCode() を実装する方法です。 値が同じなら、両者はデータベースの同じ行でなければならないため等しくなります。 (両者が Set に追加されても、 Set には1個の要素しかないことになります) 残念なことに、生成された識別子にはこのアプローチを使うことができません。 Hibernateは永続化されたオブジェクトへ識別子の値を代入するだけであり、 新しく作成されたインスタンスはどのような識別子の値も持っていません。 さらに、インスタンスがセーブされておらず、現在 Set の中にあれば、 セーブするとオブジェクトへ識別子の値を代入することになります。 もし equals()hashCode() が識別子の値に基づいているなら、 ハッシュコードが変更されると Set の規約が破られます。 この問題についての完全な議論は、Hibernateのウェブサイトを見てください。 これはHibernateの問題ではなく、オブジェクトの同一性と等価性についての、 通常のJavaのセマンティクスであることに注意してください。

ビジネスキーの等価性 を使って、 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 ( !cat.getLitterId().equals( getLitterId() ) ) return false;
        if ( !cat.getMother().equals( getMother() ) ) return false;

        return true;
    }

    public int hashCode() {
        int result;
        result = getMother().hashCode();
        result = 29 * result + getLitterId();
        return result;
    }

}

ビジネスキーはデータベースの主キー候補ほど安定である必要はないことに注意してください (項11.1.3. 「オブジェクト識別子を考える」 を見てください)。 更新不可なプロパティやユニークなプロパティは、通常ビジネスキーのよい候補です。

4.4. 動的モデル

以下の機能は現在実験段階にあると見なされており、 近い将来変更される可能性があることに注意してください。

永続エンティティは、必ずしも実行時にPOJOクラスやJavaBeanオブジェクトで表現する必要はありません。 Hibernateは(実行時に MapMap を使う)動的モデルと、 DOM4Jツリーとしてのエンティティの表現もサポートします。 このアプローチを使うと永続クラスを書かず、マッピングファイルだけを書くことになります。

デフォルトでは、Hibernateは通常のPOJOモードで動作します。 default_entity_mode 設定オプションを使って、 特定の SessionFactory に対するデフォルトのエンティティ表現モードを設定することができます (表 3.3. 「Hibernate設定プロパティ」 を見てください)。

以下の例では Map を使った表現を紹介します。 まずマッピングファイルで、クラス名の代わりに(またはそれに加えて) entity-name を定義しなければなりません:

<hibernate-mapping>

    <class entity-name="Customer">

        <id name="id"
            type="long"
            column="ID">
            <generator class="sequence"/>
        </id>

        <property name="name"
            column="NAME"
            type="string"/>

        <property name="address"
            column="ADDRESS"
            type="string"/>

        <many-to-one name="organization"
            column="ORGANIZATION_ID"
            class="Organization"/>

        <bag name="orders"
            inverse="true"
            lazy="false"
            cascade="all">
            <key column="CUSTOMER_ID"/>
            <one-to-many class="Order"/>
        </bag>

    </class>
    
</hibernate-mapping>

関連がターゲットのクラス名を使って定義していたとしても、 関連のターゲット型もPOJOではなく動的なエンティティでも構わないことに注意してください。

SessionFactory に対してデフォルトのエンティティモードを dynamic-map に設定した後、 実行時に MapMap を使うことができます:

Session s = openSession();
Transaction tx = s.beginTransaction();
Session s = openSession();

// Create a customer
Map david = new HashMap();
david.put("name", "David");

// Create an organization
Map foobar = new HashMap();
foobar.put("name", "Foobar Inc.");

// Link both
david.put("organization", foobar);

// Save both
s.save("Customer", david);
s.save("Organization", foobar);

tx.commit();
s.close();

動的なマッピングの利点は、エンティティクラスの実装を必要としないため、 プロトタイピングに要するターンアラウンドタイムが早いということです。 しかしコンパイル時の型チェックがないので、実行時に非常に多くの例外処理を扱わなければならないでしょう。 Hibernateマッピングのおかげで、データベーススキーマは容易に正規化でき、健全になり、 後で適切なドメインモデルの実装を追加することが可能になります。

エンティティ表現モードは Session ごとに設定することも可能です。

Session dynamicSession = pojoSession.getSession(EntityMode.MAP);

// Create a customer
Map david = new HashMap();
david.put("name", "David");
dynamicSession.save("Customer", david);
...
dynamicSession.flush();
dynamicSession.close()
...
// Continue on pojoSession

EntityMode を使った getSession() の呼び出しは SessionFactory ではなく Session APIにあることに注意してください。 その方法では、新しい Session は、ベースとなるJDBCコネクション、 トランザクション、その他のコンテキスト情報を共有します。 これは2番目の Session では flush()close() を呼ぶ必要がないということ、そのためトランザクションとコネクションの管理を 1番目の作業単位(Unit of Work)に任せることができるということです。

XML表現の能力についてのさらなる情報は 章 18. XMLマッピング で見つかります。

4.5. Tuplizer

org.hibernate.tuple.Tuplizer とそのサブインターフェイスは、 表現の org.hibernate.EntityMode を利用して、 データ断片のある表現の管理に責任を持ちます。 与えられたデータ断片をデータ構造として考えるなら、Tuplizerはそのようなデータ構造を どのように作成するかを知り、そのようなデータ構造からどのように値を抽出し、 注入するかを知っています。 例えばPOJOエンティティモードでは、対応するTuplizerはコンストラクタを通して、 POJOをどのように作成するか、定義されたプロパティアクセサを使い、 POJOプロパティにどのようにアクセスするかを知ります。 Tuplizerには二つのハイレベルの型があります。 それらは、org.hibernate.tuple.EntityTuplizerorg.hibernate.tuple.ComponentTuplizer インターフェイスで表現されます。 EntityTuplizer は上で述べたようなエンティティに関する契約の管理に責任を持ちます。 一方、ComponentTuplizer はコンポーネントに関する契約の管理に責任を持ちます。

ユーザは独自のTuplizerに差し替えることも可能です。 おそらくdynamic-map entity-modeの際に java.util.HashMap を使うのではなく、 java.util.Map の実装が必要でしょう。 もしくは、おそらくデフォルトのものではなく、別のプロキシ生成戦略の定義が必要でしょう。 両者とも、カスタムのTuplizer実装を定義することで達成されます。 Tuplizerの定義は、管理しようとするエンティティやコンポーネントのマッピングに結び付けられます。 顧客エンティティの例に戻ると:

<hibernate-mapping>
    <class entity-name="Customer">
        <!--
            Override the dynamic-map entity-mode
            tuplizer for the customer entity
        -->
        <tuplizer entity-mode="dynamic-map"
                class="CustomMapTuplizerImpl"/>

        <id name="id" type="long" column="ID">
            <generator class="sequence"/>
        </id>

        <!-- other properties -->
        ...
    </class>
</hibernate-mapping>


public class CustomMapTuplizerImpl
        extends org.hibernate.tuple.entity.DynamicMapEntityTuplizer {
    // override the buildInstantiator() method to plug in our custom map...
    protected final Instantiator buildInstantiator(
            org.hibernate.mapping.PersistentClass mappingInfo) {
        return new CustomMapInstantiator( mappingInfo );
    }

    private static final class CustomMapInstantiator
            extends org.hibernate.tuple.DynamicMapInstantitor {
        // override the generateMap() method to return our custom map...
        protected final Map generateMap() {
            return new CustomMap();
        }
    }
}

TODO: プロパティとプロキシパッケージのユーザ拡張フレームワークを文書化すること