Hibernate.orgCommunity Documentation

第11章 オブジェクトを扱う

11.1. Hibernate におけるオブジェクトの状態
11.2. オブジェクトを永続状態にする
11.3. オブジェクトのロード
11.4. クエリ
11.4.1. クエリの実行
11.4.2. フィルタリングコレクション
11.4.3. クライテリアのクエリ
11.4.4. ネイティブ SQL のクエリ
11.5. 永続オブジェクトの修正
11.6. detached オブジェクトの修正
11.7. 自動的な状態検出
11.8. 永続オブジェクトの削除
11.9. 異なる二つのデータストア間でのオブジェクトのレプリケーション
11.10. セッションのフラッシュ
11.11. 連鎖的な永続化
11.12. メタデータの使用

Hibernate は完全なオブジェクト/リレーショナルマッピングソリューションであり、データベース管理システムの詳細を開発者から隠蔽するだけでなく、オブジェクトの 状態管理 も行います。これは、 JDBC/SQL 永続層と同じような SQL statements の管理とは異なり、 Java アプリケーションにおける永続化に対する、とても自然なオブジェクト指向の考え方を提供します。

言いかえれば、 Hibernate を用いるアプリケーション開発者は、オブジェクトの 状態 については常に意識すべきであり、 SQL 文の実行については必ずしもそうではありません。この部分は、通常、 Hibernate が処理し、システムのパフォーマンスをチューニングするときにだけ、問題になってきます。

Hibernate は次のようなオブジェクトの状態を定義し、サポートしています:

これから、状態と状態遷移(そして、遷移のきっかけとなる Hibernate のメソッド)について、詳細に述べます。

Newly instantiated instances of a persistent class are considered transient by Hibernate. We can make a transient instance persistent by associating it with a session:

DomesticCat fritz = new DomesticCat();

fritz.setColor(Color.GINGER);
fritz.setSex('M');
fritz.setName("Fritz");
Long generatedId = (Long) sess.save(fritz);

Cat クラスの識別子が自動生成されるのであれば、 save() が呼ばれるときに、識別子が生成され、 cat インスタンスに割り当てられます。 Cat の識別子が assigned 識別子を持つか、複合キーであるなら、 save() を呼び出す前に、識別子を cat インスタンスを割り当てなければなりません。 save() の代わりに、 EJB3 の初期ドラフトで定義された persist() を使うことも可能です。

Alternatively, you can assign the identifier using an overloaded version of save().

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) );

永続化するオブジェクトが関連オブジェクトを持っている場合 (例えば、前の例における kittens コレクションのように)、外部キーカラムに、 NOT NULL 制約をつけない限りは、これらの一連のオブジェクトをどんな順番で永続化してもかまいません。外部キー制約を違反する恐れはありません。しかし、 NOT NULL 制約がある場合、間違った順番でオブジェクトを save() してしまうと、制約に違反するかもしれません。

関連するオブジェクトを自動的に保存する、 Hibernate の 遷移的な永続化 (transitive persistence) 機能を使うつもりならば、そのような詳細を気にする必要はありません。そして、 NOT NULL 制約の違反すら起こりません。 Hibernate がすべて面倒をみてくれます。遷移的な永続化は、この章の後半に書かれています。

永続化されたインスタンスの識別子があらかじめ分かっているなら、 Sessionload() メソッドを使って、復元できます。 load() は、 Class オブジェクトを引数にとり、そのクラスのインスタンスを新たに生成し、状態をロードします。そのインスタンスの状態は、永続 (persistent) 状態です。

Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// you need to wrap primitive identifiers

long id = 1234;
DomesticCat pk = (DomesticCat) sess.load( DomesticCat.class, new Long(id) );

あるいは、以下のように、既存のインスタンスに状態をロードすることもできます:

Cat cat = new DomesticCat();

// load pk's state into cat
sess.load( cat, new Long(pkId) );
Set kittens = cat.getKittens();

DB に該当する行が無い場合、 load() は回復不可能な例外を投げることに注意しましょう。そのクラスがプロキシを使ってマッピングされている場合、 load() は初期化されていないプロキシを返し、プロキシのメソッドが呼ばれるまで実際にはデータベースにアクセスしません。もし、実際にデータベースからロードせずに、オブジェクトに対する関連を作りたい場合、この振る舞いはとても役立ちます。 batch-size がクラスマッピングに定義されているならば、複数のインスタンスを一括でロードすることが可能です。

該当する行が存在することを確信できない場合は、 get() メソッドを使うべきです。それは、データベースにすぐにアクセスし、該当する行が無い場合は null を返します。

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

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

LockMode を使えば、 SELECT ... FOR UPDATE という SQL を使ってオブジェクトをロードすることができます。詳細な情報は、 API ドキュメントを参照してください。

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

関連に対するカスケード方法として lockall を指定しない限り、関連するインスタンスや含まれるコレクションは FOR UPDATE で復元 されない ことに注意しましょう。

refresh() メソッドを使うことで、どんなときでも、オブジェクトやそのコレクションをリロードすることができます。データベースのトリガがテーブルを更新した際に、そのテーブルに対応するオブジェクトのプロパティを同期する場合、このメソッドが役に立ちます。

sess.save(cat);

sess.flush(); //force the SQL INSERT
sess.refresh(cat); //re-read the state (after the trigger executes)

How much does Hibernate load from the database and how many SQL SELECTs will it use? This depends on the fetching strategy. This is explained in 「フェッチ戦略」.

探したいオブジェクトの識別子が分からない場合は、クエリが必要になります。 Hibernate は使いやすくて強力なオブジェクト指向のクエリ言語 (HQL) をサポートしています。プログラムによってクエリが作成できるように、 Hibernate は洗練された Criteria と Example クエリ機能 (QBC と QBE) をサポートしています。 ResultSet をオブジェクトに変換する Hibernate のオプション機能を使うことで、データベースのネイティブな SQL でクエリを表現することもできます。

HQL やネイティブな SQL クエリは、 org.hibernate.Query のインスタンスとして表現されます。このインタフェースは、パラメータバインディングや ResultSet のハンドリングやクエリの実行を行うメソッドを用意しています。通常、 Query は、以下に示すように、その時点の Session を使って取得します。

List cats = session.createQuery(

    "from Cat as cat where cat.birthdate < ?")
    .setDate(0, date)
    .list();
List mothers = session.createQuery(
    "select mother from Cat as cat join cat.mother as mother where cat.name = ?")
    .setString(0, name)
    .list();
List kittens = session.createQuery(
    "from Cat as cat where cat.mother = ?")
    .setEntity(0, pk)
    .list();
Cat mother = (Cat) session.createQuery(
    "select cat.mother from Cat as cat where cat = ?")
    .setEntity(0, izi)
    .uniqueResult();]]
Query mothersWithKittens = (Cat) session.createQuery(
    "select mother from Cat as mother left join fetch mother.kittens");
Set uniqueMothers = new HashSet(mothersWithKittens.list());

クエリは、普通、 list() を呼び出すことによって実行されます。クエリの結果は、メモリ上にあるコレクションにすべてロードされます。クエリによって復元されたエンティティのインスタンスは、永続状態です。もし、クエリがたった1個のインスタンスを返すと分かっているなら、 uniqueResult() メソッドが手っ取り早い方法です。即時フェッチを利用したクエリの場合、ふつう、得られたコレクションには、ルートのオブジェクトが重複して含まれています(しかし、ルートが持つコレクションは初期化 (ロード)されています)。この重複は Set を使って取り除くことができます。

Queries can also be configured as so called named queries using annotations or Hibernate mapping documents. @NamedQuery and @NamedQueries can be defined at the class level as seen in 例11.1「Defining a named query using @NamedQuery」 . However their definitions are global to the session factory/entity manager factory scope. A named query is defined by its name and the actual query string.


Using a mapping document can be configured using the <query> node. Remember to use a CDATA section if your query contains characters that could be interpreted as markup.


Parameter binding and executing is done programatically as seen in 例11.3「Parameter binding of a named query」.


実際のプログラムコードは、使われるクエリ言語に依存していないことに注意しましょう。メタデータには、ネイティブ SQL クエリを定義することもできます。また、既存のクエリをマッピングファイルに移すことで、 Hibernate に移行することもできます。

<hibernate-mapping> 要素の中のクエリ定義は、クエリに対するユニークな名前が必要なことにも注意してください。それに対して、 <class> 要素の中のクエリ定義は、クラスの完全限定名が前に付けられるので、自動的にユニークな名前になります。例: eg.Cat.ByNameAndMaximumWeight

コレクション フィルタ は、永続化されているコレクションや配列に適用される特殊なタイプのクエリです。そのクエリ文字列では、コレクションのその時点での要素を意味する this を使います。

Collection blackKittens = session.createFilter(

    pk.getKittens(), 
    "where this.color = ?")
    .setParameter( Color.BLACK, Hibernate.custom(ColorUserType.class) )
    .list()
);

返されるコレクションは Bag とみなされます。そして、それはもとのコレクションのコピーになります。元のコレクションは修正されません(これは、 "filter" という名前の意味とは異なりますが、期待される動きとは一致しています)。

フィルタには from 節が不要であることに気づくでしょう(必要なら、持つことも可能ですが)。フィルタは、コレクションの要素自体を返して構いません。

Collection blackKittenMates = session.createFilter(

    pk.getKittens(), 
    "select this.mate where this.color = eg.Color.BLACK.intValue")
    .list();

クエリを含まないフィルタも役に立ちます。例えば、非常に大きなコレクションの部分集合をロードするために使えます。

Collection tenKittens = session.createFilter(

    mother.getKittens(), "")
    .setFirstResult(0).setMaxResults(10)
    .list();

処理中の永続インスタンス (例: Session によって、ロード、セーブ、作成、クエリされたオブジェクト)は、アプリケーションに操作されます。その際に変更された永続状態は、 Sessionフラッシュ されるときに、永続化されます(これは、この章の後半で述べています)。変更を永続化するために、特殊なメソッド( update() のようなもの。これは、別の目的で使用します)を呼ぶ必要はありません。オブジェクトの状態を更新する一番簡単な方法は、オブジェクトを load() し、 Session をオープンにしている間に、直接操作することです。

DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) );

cat.setName("PK");
sess.flush();  // changes to cat are automatically detected and persisted

(オブジェクトをロードするための) SQL の SELECT と(更新された状態を永続化するための) SQL の UPDATE が同じセッションで必要となるので、このプログラミングモデルは、効率が悪くなる場合があります。そのため、 Hibernate は別の方法を用意しています。それは、 detached インスタンスを使用する方法です。

多くのアプリケーションでは次のことが必要になります。それは、あるトランザクションでオブジェクトを復元し、操作するためにそれを UI 層に送り、その後に、新しいトランザクションで変更をセーブするといったことです。並行性の高い環境で、このタイプのアプローチを使うアプリケーションでは、「期間の長い」作業単位の隔離性を保証するために、バージョンデータが通常使われます。

Hibernate は、 Session.update()Session.merge() メソッドを使って、 detached インスタンスを再追加することで、このモデルに対応します。

// in the first session

Cat cat = (Cat) firstSession.load(Cat.class, catId);
Cat potentialMate = new Cat();
firstSession.save(potentialMate);
// in a higher layer of the application
cat.setMate(potentialMate);
// later, in a new session
secondSession.update(cat);  // update cat
secondSession.update(mate); // update mate

識別子 catId を持つ Cat が、既に secondSession でロードされていた場合は、再追加しようとしたときに、例外が投げられます。

同じ識別子を持つ永続インスタンスをセッションが既に保持していないことを確信できるなら update() を使います。そして、セッションの状態を考えずに、どんな場合でも変更をマージしたい場合は、 merge() を使います。すなわち、 detached インスタンスの再追加操作が、最初に実行されることを確実にするために、通常は update() が新しいセッションのなかで最初に呼ばれるメソッドになります。

The application should individually update() detached instances that are reachable from the given detached instance only if it wants their state to be updated. This can be automated using transitive persistence. See 「連鎖的な永続化」 for more information.

lock() メソッドでもまた、新しいセッションにオブジェクトを再関連付けできます。しかし、 detached インスタンスは無修正でなければなりません。

//just reassociate:

sess.lock(fritz, LockMode.NONE);
//do a version check, then reassociate:
sess.lock(izi, LockMode.READ);
//do a version check, using SELECT ... FOR UPDATE, then reassociate:
sess.lock(pk, LockMode.UPGRADE);

lock() は、さまざまな LockMode とともに使うことができます。詳細は、 API ドキュメントとトランザクション処理の章を参照してください。再追加のときにだけ、 lock() が使われるわけではありません。

Other models for long units of work are discussed in 「楽観的同時実行制御」.

Hibernate のユーザーは次の2つのケースのどちらにも使える汎用的なメソッドを要求していました。それは、新しい識別子を生成して transient インスタンスをセーブすることと、その時点の識別子と関連づいている detached インスタンスを更新/再追加することのできるメソッドです。 saveOrUpdate() はこのような機能を実現したメソッドです。

// in the first session

Cat cat = (Cat) firstSession.load(Cat.class, catID);
// in a higher tier of the application
Cat mate = new Cat();
cat.setMate(mate);
// later, in a new session
secondSession.saveOrUpdate(cat);   // update existing state (cat has a non-null id)
secondSession.saveOrUpdate(mate);  // save the new instance (mate has a null id)

saveOrUpdate() の使用方法と意味は、新しいユーザーにとって混乱を招くかもしれません。まず第一に、あるセッションで使用したインスタンスを別の新しいセッションで使おうとしない限り、 update()saveOrUpdate()merge() を使う必要はありません。アプリケーション全体を通じて、これらのメソッドを全く使わないこともあります。

通常、 update()saveOrUpdate() は次のシナリオで使われます:

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

そして、 merge() は以下のように非常に異なります:

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

sess.delete(cat);

外部キー制約に違反するリスクもなく、好きな順番でオブジェクトを削除することができます。ただし、間違った順番でオブジェクトを削除すると、外部キーカラムの NOT NULL 制約に違反する可能性があります。例えば、親オブジェクトを削除したときに、子供オブジェクトを削除し忘れた場合です。

永続インスタンスのグラフを別のデータストアに永続化する場合に、識別子の値を再生成せずにすむと便利な場合があります。

//retrieve a cat from one database

Session session1 = factory1.openSession();
Transaction tx1 = session1.beginTransaction();
Cat cat = session1.get(Cat.class, catId);
tx1.commit();
session1.close();
//reconcile with a second database
Session session2 = factory2.openSession();
Transaction tx2 = session2.beginTransaction();
session2.replicate(cat, ReplicationMode.LATEST_VERSION);
tx2.commit();
session2.close();

レプリケーション先のデータベースに行が既にある場合、 replicate() が衝突をどのように扱うかを ReplicationMode で指定します。

次のようなケースで、この機能を使用します。異なるデータベースインスタンスに入れられたデータの同期、製品更新時におけるシステム設定情報の更新、非 ACID トランザクションのなかで加えられた変更のロールバックなどです。

JDBC コネクションの状態とメモリ上のオブジェクトの状態を同期させるために必要な SQL 文を Session が実行することがときどきあります。この処理 flush は、デフォルトでは次のときに起こります。

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

(1つ例外があります。 native ID 生成を使ったオブジェクトは、それらがセーブされたときに挿入されます。)

明示的に flush() するときを除いて、 いつ Session が JDBC をコールするのかについて絶対的な保証はありません。ただし、それらが実行される 順番 だけは保証されます。また、 Hibernate は、 Query.list(..) が古いデータや間違ったデータ返さないことを保証しています。

It is possible to change the default behavior so that flush occurs less frequently. The FlushMode class defines three different modes: only flush at commit time when the Hibernate Transaction API is used, flush automatically using the explained routine, or never flush unless flush() is called explicitly. The last mode is useful for long running units of work, where a Session is kept open and disconnected for a long time (see 「拡張セッションと自動バージョニング」).

sess = sf.openSession();

Transaction tx = sess.beginTransaction();
sess.setFlushMode(FlushMode.COMMIT); // allow queries to return stale state
Cat izi = (Cat) sess.load(Cat.class, id);
izi.setName(iznizi);
// might return stale data
sess.find("from Cat as cat left outer join cat.kittens kitten");
// change to izi is not flushed!
...
tx.commit(); // flush occurs
sess.close();

During flush, an exception might occur (e.g. if a DML operation violates a constraint). Since handling exceptions involves some understanding of Hibernate's transactional behavior, we discuss it in 13章Transactions and Concurrency.

個々のオブジェクトをセーブしたり、削除したり、再追加したりすることはかなり面倒です。特に、関連するオブジェクトを扱うような場合には際立ちます。よくあるのは、親子関係を扱うケースです。以下の例を考えてみましょう:

もし、親子関係の子が値型なら(例えば、住所や文字列のコレクション)、それらのライフサイクルは親に依存しており、便利な状態変化の「カスケード」を使うために、追加の作業は必要はありません。親がセーブされたとき、値型の子オブジェクトも同じようにセーブされますし、親が削除されたときは、子も削除されます。その他の操作も同じです。コレクションから1つの子を削除するような操作でもうまくいきます。すなわち、 Hibernate はこの削除操作を検出すると、値型のオブジェクトは参照を共有できないので、データベースからその子供を削除します。

ここで、親と子が値型でなくエンティティであるとして同じシナリオを考えてみましょう。(例えば、カテゴリーと品目の関係や親と子の猫の関係です。)エンティティは、それ自身がライフサイクルを持ち、参照の共有をサポートします。(そのため、コレクションからエンティティを削除することは、エンティティ自身の削除を意味しません。)また、エンティティは、デフォルトでは、関連する他のエンティティへ状態をカスケードすることはありません。 Hibernate は 到達可能性による永続化 をデフォルトでは実行しません。

Hibernate の Session の基本操作( persist(), merge(), saveOrUpdate(), delete(), lock(), refresh(), evict(), replicate() が含まれます)に対して、それぞれに対応するカスケードスタイルがあります。それぞれのカスケードスタイルには、 create, merge, save-update, delete, lock, refresh, evict, replicate という名前がついています。もし、関連に沿ってカスケードさせたい操作があるなら、マッピングファイルにそう指定しなければなりません。例えば、以下のようにします:


<one-to-one name="person" cascade="persist"/>

カスケードスタイルは、組み合わせることができます:


<one-to-one name="person" cascade="persist,delete,lock"/>

すべての 操作を関連に沿ってカスケードするよう指定するときは、 cascade="all" を使います。デフォルトの cascade="none" は、どの操作もカスケードしないことを意味します。

In case you are using annotatons you probably have noticed the cascade attribute taking an array of CascadeType as a value. The cascade concept in JPA is very is similar to the transitive persistence and cascading of operations as described above, but with slightly different semantics and cascading types:

A special cascade style, delete-orphan, applies only to one-to-many associations, and indicates that the delete() operation should be applied to any child object that is removed from the association. Using annotations there is no CascadeType.DELETE-ORPHAN equivalent. Instead you can use the attribute orphanRemoval as seen in 例11.4「@OneToMany with orphanRemoval」. If an entity is removed from a @OneToMany collection or an associated entity is dereferenced from a @OneToOne association, this associated entity can be marked for deletion if orphanRemoval is set to true.


おすすめ:

  • It does not usually make sense to enable cascade on a many-to-one or many-to-many association. In fact the @ManyToOne and @ManyToMany don't even offer a orphanRemoval attribute. Cascading is often useful for one-to-one and one-to-many associations.

  • If the child object's lifespan is bounded by the lifespan of the parent object, make it a life cycle object by specifying cascade="all,delete-orphan"(@OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)).

  • それ以外の場合は、カスケードはほとんど必要ないでしょう。しかし、同じトランザクションのなかで親と子が一緒に動作することが多いと思い、いくらかのコードを書く手間を省きたいのであれば、 cascade="persist,merge,save-update" を使うことを考えましょう。

cascade="all" でマッピングした関連(単値関連やコレクション)は、 親子 スタイルの関連とマークされます。それは、親のセーブ/更新/削除が、子のセーブ/更新/削除を引き起こす関係のことです。

Furthermore, a mere reference to a child from a persistent parent will result in save/update of the child. This metaphor is incomplete, however. A child which becomes unreferenced by its parent is not automatically deleted, except in the case of a one-to-many association mapped with cascade="delete-orphan". The precise semantics of cascading operations for a parent/child relationship are as follows:

  • 親が persist() に渡されたならば、すべての子は persist() に渡されます。

  • merge() に渡されたならば、すべての子は merge() に渡されます。

  • 親が save()update()saveOrUpdate() に渡されたならば、すべての子は saveOrUpdate() に渡されます。

  • transient または detached の子が、永続化された親に参照されたならば、 saveOrUpdate() に渡されます。

  • 親が削除されたならば、すべての子は、 delete() に渡されます。

  • 子が永続化された親から参照されなくなったときは、 特に何も起こりません 。よって、アプリケーションが必要であれば、明示的に削除する必要があります。ただし、 cascade="delete-orphan" の場合を除きます。この場合、「親のない」子は削除されます。

最後に、操作のカスケードがオブジェクトグラフに適用されるのは、 コールした時 あるいは、 flushした時 であることに注意してください。すべての操作は、その操作が実行されたときに、到達可能な関連するエンティティに対してカスケードが可能ならカスケードします。しかし、 save-upatedelete-orphan は、 Session が flush している間に、すべての到達可能な関連するエンティティに伝播します。

Hibernate は、すべてのエンティティと値型の非常にリッチなメタレベルのモデルを必要とします。ときどき、このモデルはアプリケーションにとってとても役に立ちます。例えば、アプリケーションは、 Hibernate のメタデータを使って、「賢い」ディープコピーアルゴリズムを実装できるかもしません。そのアルゴリズムとは、どのオブジェクトがコピーされるべきか(例:可変の値型)やどのオブジェクトはコピーされないべきか(例:不変な値型や可能なら関連するエンティティ)を判断できるものです。

Hibernate は ClassMetadataCollectionMetadata インタフェースと Type 階層を通してメタデータを公開します。メタデータインターフェースのインスタンスは、 SessionFactory から得られます。

Cat fritz = ......;

ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
Object[] propertyValues = catMeta.getPropertyValues(fritz);
String[] propertyNames = catMeta.getPropertyNames();
Type[] propertyTypes = catMeta.getPropertyTypes();
// get a Map of all properties which are not collections or associations
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] );
    }
}