Hibernate は O/R マッピングツールと呼ばれる、リレーショナルデータベースとオブジェクトモデルとの間を埋めるフレームワークです。
関連するテーブルのオブジェクトを管理する方法が、十数種類あります。Index of Relationships のサイトに、Hibernate でサポートされる関係の一覧が載っています。非常に分かりやすかったのですが、さらに理解を深めるために、車輪の再発明をしてみようと思います。
こちらのサイトでも、Hibernate 入門記 - koichik」 はすごい ・・・ (^^;
参考
Hibernate がサポートする関係
- One-to-One (一対一)関係
- Many-to-One (多対一)関係
- One-to-Many (一対多)関係
- Many-to-Many (多対多)関係
- Collection 単一列値 関係
- 継承 マッピング (table per class)
- Composite マッピング
- 再帰的な関連
- そのほかの関係
- コレクションに関して
- Open Session in View
One-to-One (一対一)関係
One-to-One は同一の主キーを持つテーブルの関係を表します。派生関係とも言います。同一の主キーを持つテーブル同士の関係をマッピングできます。関連元の主キーと関連先の主キーが同じものを関連にします。
マッピングは次のように行います。
<hibernate-mapping>
<class
name="com.hamasyou.model.Product"
table="PRODUCT">
...
<one-to-one
name="attribute"
class="com.hamasyou.model.ProductAttribute"
cascade="all"
outer-join="auto"
constrained="false" />
...
</class>
</hibernate-mapping>
XDoclet で書くとこうなります。関連元のクラスから @hibernate.one-to-one を指定しています。
Product.java
/**
* 販売品目属性
* @return
*
* @hibernate.one-to-one
* class="com.hamasyou.model.ProductAttribute"
* cascade="all"
*/
public ProductAttribute getAttribute() {
関連先の ProductAttribute クラスは特別なタグは必要ありません。通常の @hibernate.class タグでテーブルと永続化クラスのマッピングを指定して、主キーのカラムに @hibernate.id を指定するだけです。
ProductAttribute.java
/**
* ProductAttribute
*
* @hibernate.class
* table="PRODUCT_ATTR"
*/
public class ProductAttribute implements Serializable {
... 中略 ...
/**
* 品目No
* @return
*
* @hibernate.id
* column="PRODUCT_NO"
* generator-class="assigned"
*/
public Integer getProductNo() {
品目 (Product) と 販売品目属性 (ProductAttribute) は同じ主キーを持つので、One-to-One の関係をもてます。今回は、品目のほうに One-to-One の関係を持たせたので、品目のインスタンスを取得した時点で、販売品目属性のインスタンスも検索します。
カスケード (cascade) 属性は、オブジェクトに対して行った処理を関連オブジェクトにまで伝播するかどうかを指定するものです。 all、none、save-update、delete を指定できます。 all は更新と削除の両方を、save-update は更新を、delete は削除をそれぞれ伝播させます。
[ソースコード実行後の結果画面]
[品目No:1 名前:パソコン] [品目No:1 原価:300000] [品目No:2 名前:冷蔵庫] [品目No:2 原価:130000] [品目No:3 名前:エアコン] [品目No:3 原価:80000] [品目No:4 名前:テレビ] [品目No:4 原価:40000]
[ソースコード]
Many-to-One (多対一)関係
Many-to-One は参照関係を持つテーブルの関係を表します。外部キーを使って参照するときに使います。外部キーを持つ側から見た場合の関係です。
マッピングは次のように行います。
<hibernate-mapping>
<class
name="com.hamasyou.model.Product"
table="PRODUCT">
...
<many-to-one
name="category"
class="com.hamasyou.model.Category"
cascade="all"
outer-join="auto"
update="true"
insert="true"
access="property"
column="CATEGORY_NO"
not-null="true" />
...
</class>
</hibernate-mapping>
XDoclet で書くとこうなります。
Product.java
/**
* カテゴリ
* @return
*
* @hibernate.many-to-one
* column="CATEGORY_NO"
* class="com.hamasyou.model.Category"
* not-null="true"
* cascade="all"
*/
public Category getCategory() {
column 属性でテーブルのどのカラムが関連先の主キーと関連付けされているかを指定しています。外部キーを指定していると言ったほうが分かりやすいですね。Category クラスには、特別なタグは不要です。 @hibernate.class タグでテーブルを指定して、 @hibernate.id で主キーを指定するだけです。
Many-to-One の関係も、One-to-One と同じく、品目 (Product) オブジェクトが検索された時点で、カテゴリ (Category) オブジェクトも検索されます。カスケード設定を行うことで、外部キーの参照先 (カテゴリオブジェクト) から登録されます。
結果画面を見ると面白いことに、カテゴリオブジェクトは、同一のカテゴリの場合には、一つのインスタンスしか生成されていません。
[ソースコード実行後の結果画面]
[品目No:1名前:HibernateinAction@127fa12][カテゴリNo:1名前:本@17f409c] [品目No:2名前:SpringinAction@1c05ffd][カテゴリNo:1名前:本@17f409c] [品目No:3名前:パソコン@de1b8a][カテゴリNo:2名前:家電@18bbc5a] [品目No:4名前:テレビ@1e232b5][カテゴリNo:2名前:家電@18bbc5a] [品目No:5名前:長袖の服@16f144c][カテゴリNo:3名前:衣服@13c6a22]
[ソースコード]
One-to-Many (一対多)関係
One-to-Many は親子関係を持つテーブルを表します。オブジェクトから返されるものがコレクション型になります。遅延ロードを行うことができます。コレクション型は、基本的には Set を使うのが推奨されます。Set は一意なインスタンスの集合を表すからです。
マッピングは次のように行います。
<hibernate-mapping>
<class
name="com.hamasyou.model.Order"
table="ORDER_HEADER">
...
<set
name="specifics"
table="ORDER_SPECIFIC"
lazy="true"
inverse="false"
cascade="all"
sort="unsorted">
<key column="ORDER_NO"/>
<one-to-many
class="com.hamasyou.model.OrderSpecific" />
</set>
...
</class>
</hibernate-mapping>
XDoclet ではこう書きます。
Order.java
/**
* 受注明細
* @return
*
* @hibernate.set
* table="ORDER_SPECIFIC"
* cascade="all"
* lazy="true"
* @hibernate.collection-key
* column="ORDER_NO"
* @hibernate.collection-one-to-many
* class="com.hamasyou.model.OrderSpecific"
*/
public Set getSpecifics() {
今回は、受注明細クラス (OrderSpecific.java) との関係が親子関係であったので、Set を使いました。Set、Bag をコレクション型として使う場合 (@hibernate.set, @hibernate.bag) は、@hibernate.collection-key を指定する必要があります。コレクション要素が一意になることを保証するキーを指定しなければなりません。今回は、ORDER_SPECIFIC テーブルの ORDER_NO カラムをキーに指定しました。
今回のように、関連先が複数のインスタンスになる場合、遅延ロードという手法をとることが出来ます。インスタンスの生成を、最初のアクセスまで遅らせることが出来る機能です。 lazy 属性 に true を指定することで、遅延ロードが出来るようになります。
- 遅延ロードの注意
-
遅延ロードする場合の注意点として、遅延ロードされるオブジェクトがロードされる際には、親オブジェクトに有効なセッションが関連付けられていなければなりません。
Webアプリケーションにおける遅延ロードの問題点は「Open Session in View」 をご覧ください。
受注明細クラスに、複合キー (composite-id) というものをつかいました。これは、複数のキーをあわせて主キーにするためのものです。複合キーを使うには、複合キー用のクラスを用意する必要があります。今回は、 OrderSpecificID というクラスを複合キークラスとして使用しました。
複合キークラスに必要な特性として下記の2点があります。
複合キークラスが必要な特性
- java.io.Serializable を実装しなければならない
- equals と hashCode をオーバーライドしなければならない
これは、複合キーが一意な値を表現するために必要なことになります。
XDoclet で複合キーを表現するには、複合キークラスを作成し、それを @hibernate.id タグで関連付ければいいだけです。
OrderSpecific.java
/**
* 受注明細の主キー
* @return
*
* @hibernate.id
* generator-class="assigned"
* type="com.hamasyou.model.type.OrderSpecificID"
*/
public OrderSpecificID getSpecificId() {
return specificId;
}
public void setSpecificId(OrderSpecificID specificId) {
this.specificId = specificId;
}
OrderSpecificID クラスのプロパティには、@hibernate.property タグが指定されています。@hibernate.class タグは必要ありません。これはつまり、OrderSpecificID クラスは、OrderSpecific クラスから使われると言うことがわかっているということになります。
[ソースコード実行後の結果画面]
[受注No:1受注日:2004-08-05 00:00:00.0@16acdd1] └ [受注No:1受注行No:1値段:1000@149d886] └ [受注No:1受注行No:3値段:920@1fe88d] └ [受注No:1受注行No:2値段:1500@1267649] [受注No:2受注日:2004-04-0700:00:00.0@7cbde6] └ [受注No:2受注行No:1値段:150@148662] └ [受注No:2受注行No:2値段:300@1829e6f] [受注No:3受注日:2004-11-1800:00:00.0@1977b9b] └ [受注No:3受注行No:1値段:5000@180f96c] └ [受注No:3受注行No:2値段:3000@7736bd]
[ソースコード]
Many-to-Many (多対多)関係
Many-to-Many (多対多)関係 は関係テーブルを表します。多重度が互いに 0以上 の場合、導出されるテーブルだと言えます。ログテーブルなんかも、Many-to-Many の関係になると思います。
マッピングは次のように行います。
<hibernate-mapping>
<class
name="com.hamasyou.model.Employee"
table="EMPLOYEE">
...
<set
name="projects"
table="PROJECT_ASSIGN"
lazy="true"
inverse="false"
cascade="all"
sort="unsorted">
<key column="EMP_NO" />
<many-to-many
class="com.hamasyou.model.Project"
column="PROJECT_NO"
outer-join="auto" />
</set>
...
</class>
</hibernate-mapping>
多重度が、相互に多対多の場合、関連テーブルと言うものを作って管理します。しかし、オブジェクト指向設計では関連テーブルと言うのは普通は意識しません。その証拠に、今回はクラスにプロジェクト割り当て (PROJECT_ASSIGN) 用のクラスが出てきませんでした。
逆にオブジェクトモデリングにおいては、関連クラスと言うものを導出する場合があります。関連クラスは、関連に意味がある場合に導出されます。通常、関連クラスは操作を持ちます。そういう場合は、クラスとして作成されます。関連クラスを作成した場合は、関連クラスと各々のクラスとで、Many-to-One の関係を定義すればよさそうです。
One-to-Many や Many-to-Many の関係では、片方のクラスから、他方のクラスのコレクションを返すメソッド用意しました。コレクションを定義する場合のマッピングを書くときには、以下の3つの点を守ればよさそうです。
<ol
XDoclet で書くと、こんな感じになります。
Employee.java
/**
* 割り当てプロジェクト
* @return
*
* @hibernate.set
* cascade="all"
* lazy="true"
* table="PROJECT_ASSIGN"
* @hibernate.collection-key
* column="EMP_NO"
* @hibernate.collection-many-to-many
* column="PROJECT_NO"
* class="com.hamasyou.model.Project"
*/
public Set getProjects() {
Project.java
/**
* 割り当てられた社員
*
* @hibernate.set
* inverse="true"
* cascade="all"
* lazy="true"
* table="PROJECT_ASSIGN"
* @hibernate.collection-key
* column="PROJECT_NO"
* @hibernate.collection-many-to-many
* column="EMP_NO"
* class="com.hamasyou.model.Employee"
*/
public Set getEmployees() {
[Employee.java]5行目がコレクション型の指定です。8行目が自クラスの主キーに対応させる関係テーブルのカラム指定です。PROJECT_ASSIGN テーブルの EMP_NO カラムと自クラスの主キーを対応させています。11行目が、相手クラスとどういう関係かをあわしています。多対多の関係ですので、many-to-many を使っています。Projectクラスの主キーを関係テーブルのどのカラムにマッピングさせるかと言うことも指定しています。
[Project.java]5行目のinverse属性がポイントです。 many-to-many の関係は双方向関連になるので、Hibernateにどちらが逆関連かを教えてやることができます。どちらの方向が逆になるかはあまり深く考えなくてもいいようです。
Many-to-Many の関係の場合、プロジェクト割り当て (PROJECT_ASSIGN) テーブル用のクラスを作っていなかったので、3行目のコレクション指定時に table 属性でテーブル名を指定しました。また、many-to-many の関係では、 相手のクラスの主キーが、関連クラスのどのカラムに対応するかと言うことも指定しなければなりません。 @hibernate.collection-many-to-many で column を指定するのはそういう理由です。
[ソースコード実行後の結果画面]
---社員からプロジェクトを検索--- [社員No:1名前:山田太郎@ee6681] └[プロジェクトNO:1名前:翻訳プロジェクト@147c1db] └[プロジェクトNO:3名前:システム開発X@82d37] [社員No:2名前:佐藤次郎@2f0df1] └[プロジェクトNO:2名前:社内開発A@1f3ce5c] └[プロジェクトNO:1名前:翻訳プロジェクト@147c1db] [社員No:3名前:加藤三郎@13c6a22] └[プロジェクトNO:3名前:システム開発X@82d37] [社員No:4名前:榊原四郎@15c07d8] ---プロジェクトから社員を検索--- [プロジェクトNO:1名前:翻訳プロジェクト@147c1db] └[社員No:2名前:佐藤次郎@2f0df1] └[社員No:1名前:山田太郎@ee6681] [プロジェクトNO:2名前:社内開発A@1f3ce5c] └[社員No:2名前:佐藤次郎@2f0df1] [プロジェクトNO:3名前:システム開発X@82d37] └[社員No:3名前:加藤三郎@13c6a22] └[社員No:1名前:山田太郎@ee6681]
[ソースコード]
Collection 単一列値 関係
Collection 単一列値 関係 は、親子関係または参照関係にあるテーブルの特定のカラムだけを保持するコレクションを扱う関係です。
マッピングは次のように行います。
<list
name="moneys"
table="ORDER_SPECIFIC"
lazy="true"
inverse="false"
cascade="none">
<key column="ORDER_NO" />
<index
column="SPECIFIC_NO"
type="integer" />
<element
column="MONEY"
type="java.lang.Integer"
not-null="false"
unique="false" />
</list>
XDoclet で書くと、大体こうなります。
/**
* 金額リスト
* @return
*
* @hibernate.list
* lazy="true"
* table="ORDER_SPECIFIC"
* @hibernate.collection-key
* column="ORDER_NO"
* @hibernate.collection-index
* column="SPECIFIC_NO"
* type="java.lang.Integer"
* @hibernate.collection-element
* column="MONEY"
* type="java.lang.Integer"
*/
public List getMoneyList() {
単一列値を生データとして取り出す関係です。MONEY 列は、ユニークではないので、コレクションに list を使いました。
コレクションを使う場合は、自クラスの主キーとマッピングする列を、key として指定します。XDoclet の場合は @hibernate.collection-key がそれにあたります。相手のクラスの外部キーの列を指定します。
コレクションに含める値を element として指定します。XDoclet では @hibernate.collection-element で指定しました。MONEY 列を java.lang.Integer として格納すると支持しています。getMoneyList() の戻り値 List には、Integer 型でコレクション要素が入ります。
- リスト型の注意点
-
コレクションの型を list にしたときは、index 要素 (XDoclet では @hibernate.collection-index) を指定しなければいけません。これは、Foo[i] の i の部分を保持するテーブル上のカラムです。
今回は、SPECIFIC_NO 列が、明細行ごとにシーケンシャルな値を振られることにしているので、この列を指定しました。シーケンシャル値は 0 から格納しなければなりません。 そうしないと、シーケンス番号がない部分には null が格納されることになります。
[ソースコード]
継承 マッピング (table per class)
継承 マッピングは、テーブル内のカラム値によってサブクラスを変えるような関係です。継承関係のクラスを単一のテーブルにマッピングします。そのため、サブクラスを判断するカラムが必要になります。
マッピングは次のように行います。
<class
name="com.hamasyou.model.Product"
table="PRODUCT"
dynamic-update="false"
dynamic-insert="false"
select-before-update="false"
optimistic-lock="version"
discriminator-value="0">
...
<discriminator column="CATEGORY" />
...
<subclass
name="com.hamasyou.model.ElectricProduct"
dynamic-update="false"
dynamic-insert="false"
discriminator-value="1">
</subclass>
<subclass
name="com.hamasyou.model.ClothingProduct"
dynamic-update="false"
dynamic-insert="false"
discriminator-value="2">
</subclass>
</class>
XDoclet で書くと、大体こうなります。Product クラスが スーパークラスで、ElectricProduct と ClothingProduct がサブクラスになっています。
Product.java
/**
* Product
*
* @hibernate.class
* table="PRODUCT"
* discriminator-value="0"
* @hibernate.discriminator
* column="CATEGORY"
*/
public abstract class Product implements Serializable {
サブクラスはこんな感じです。
ElectricProduct & ClothingProduct
/**
* ElectricProduct
*
* @hibernate.subclass
* discriminator-value="1"
*/
public class ElectricProduct extends Product {
...
/**
* ClothingProduct
*
* @hibernate.subclass
* discriminator-value="2"
*/
public class ClothingProduct extends Product {
テーブル内の特定のカラムの値によって、生成するサブクラスを切り替えることが出来ます。
特徴的なのは、 discriminator タグと subclass タグです。XDoclet では @hibernate.discriminator と @hibernate.subclass です。discriminator で指定されたカラムの値をサブクラスの切り替え材料にします。
XDoclet ではスーパークラスに @hibernate.discriminator タグを指定します。column タグで指定したカラム値を元に、@hibernate.subclass discriminator-value で指定された値と一致するサブクラスが生成されます。
- net.sf.hibernate.WrongClassException がスローされる場合
-
discriminator-value で指定されていない値がデータベースに格納されていた場合、エラーになります。ただし、スーパークラスの @hibernate.discriminator の force 属性を true にすることで、discriminator-value で指定されていない値がデータベースに格納されていた場合は、インスタンスを復元しなくなります。
[出力される例外]
net.sf.hibernate.WrongClassException
discriminator-value
「discriminator-value が指定したもの以外はこのサブクラスを使う」といった指定は出来なさそうです。discriminator-value を複数取ることも出来ませんでした。また、discriminator-value は大文字・小文字の区別をするようです。
[ソースコード]
Composite マッピング
Composite マッピングは、コンポジションモデルを一つのテーブルにマッピングする方法です。今回の例では、住所オブジェクトは、会社オブジェクトにコンポジションされています。が、住所オブジェクト用の独立したテーブルは存在せず、会社テーブルの一部となっています。
マッピングは次のように行います。
<class
name="com.hamasyou.model.Company"
table="COMPANY"
dynamic-update="false"
dynamic-insert="false"
select-before-update="false"
optimistic-lock="version">
....
<component
name="address"
class="com.hamasyou.model.Address">
<property
name="city"
type="java.lang.String"
update="true"
insert="true"
access="property"
column="CITY"
not-null="true" />
<property
name="prefectural"
type="java.lang.String"
update="true"
insert="true"
access="property"
column="PREFECTURAL"
not-null="true" />
<property
name="zip"
type="java.lang.String"
update="true"
insert="true"
access="property"
column="ZIP"
not-null="true" />
</component>
....
</class>
XDoclet で書くと、大体こうなります。Address クラスは JavaBean で、getter / setter には @hibernate.property タグが指定されています。@hibernate.class タグは必要ありません。
/**
* 住所
* @return
*
* @hibernate.component
* class="com.hamasyou.model.Address"
*/
public Address getAddress() {
ライフサイクルが同じオブジェクトは、コンポジションとしてモデリングされることが多いです。コンポジットマッピングはコンポジットモデルを、単一のテーブルに割り当てる場合に使われます。
特別難しいものはなく、コンポジットの親となる 会社クラスの内部に 住所オブジェクトを持たせるだけです。XDoclet の指定では、getAddress() メソッドに @hibernate.component タグを指定するだけです。
One-to-One 関係との違いは、単純にテーブルが分かれているか分かれていないかな気がします。
[ソースコード]
再帰的な関連
再帰的な関連とは自分自身への関連を持っているような場合です。ツリー型を構成するようなオブジェクトの関係がそうです。
この関係を表すには、子要素のオブジェクトからの視点を持ってマッピングファイルを作ることで可能になります。つまり、親となるレコードのIDを持つようにすればいいのです。マッピングファイルでは many-to-one の関係になります。
<hibernate-mapping>
<class name="com.hamasyou.hibernate.Organization"
table="Organization">
...
<many-to-one
name="parentOrganization"
class="com.hamasyou.hibernate.Organization"
column="parent_organization_id"/>
...
</class>
</hibernate-mapping>
このように、自クラスに対して many-to-one の関係を指定してあげることで、親クラスへの参照を内部に持つようになります。
そのほかの関係
他の関係については、『Hibernate - マッピング体験記』 に非常に詳しく載っているので、そっちを参考にすることにします。参考にしたい関係があれば、随時更新するつもりです。
コレクションに関して
基本的にコレクション型は、Setか Bag を利用するのが良いようです。Javaでよく使われる List は、Hibernate では、テーブルにインデックス用のカラム (Foo[i] の i を保持するカラム) がなければ利用不可能です。例えば、シーケンシャルなIDみたいなものを順次インクリメントする場合であれば利用できます。
Set は一意な値を保持することを保証するコレクションなので、主キーを保持するオブジェクト型を格納するときには、これを使います。Bag は複数回格納されたことを保持する Set の派生と考えればよさそうです。
List は、Foo[i] の i を保持するカラムがテーブルに存在しなければ使えません。順次インクリメントされる ID のようなカラムがあれば使用できます。その際、ID は 0から始まるようにしなければいけません。さもなければ、足らない部分に null が挿入された List が出来上がってしまいます。
コレクション要素を使用する場合は、key 属性 (XDoclet では @hibernate.collection-key) が必須項目になります。また、インデックスを使って要素にアクセスする Map, List, 配列 は、index属性 (XDoclet では @hibernate.collection-index) が必須になります。Set, Bag では必要ありません。コレクションが Map の場合は index-many-to-many (XDoclet では @hibernate.index-many-to-many) を使う。
Open Session in View
Open Session in View とは、ビューで Session の開始と終了を管理するパターンです。Hibernate は遅延ロード (lazy load) をサポートしています。この遅延ロードは、 Session が開かれていなければならないと言う条件があります。JSP + Servlet + JavaBean という形で開発を行った場合、遅延ロードは多くの場合 JSP でアクセスされたときに最初にロードされるようになります。
Session の開始と終了をビューで行わなければ、JSP で最初にアクセスされた場合に例外が発生してしまいます。これを防ぐのが「Open Session in View パターン」と言われるものです。具体的なコードは、Hibernate.org にサンプルが載っています。
[参考]
実装には、サーブレットフィルタを利用します。リクエストがきたときに Session を開き、レスポンスを返すときに Session を閉じます。HIbernate は Session にオブジェクトのキャッシュを持ちますので、リクエストの最初から最後まで同じ Session を使いまわすことはパフォーマンスの向上が期待できます。
参考
Hibernate のリファレンスドキュメント (日本語) Hibernate Reference Document
Hibernate で使える関係を全部まとめて紹介しています。 Hibernate - マッピング体験記
Hibernate だけでなく、データベース設計にまで触れられていておすすめ。
- 薄いながらも十分な情報量。HibernateとSpringにも触れられています。
- Hibernate の基本的な使い方が載っています。
- 開発者のための Hibernate の解説書が日本語で登場しました。