|
|
||
index-many-to-many(Entity Map)を使った"Ternary Associations" mappingの実現に関するメモ*1.
■"Ternary Associations" mappingとは
CREATE TABLE FOO (
ID INTEGER NOT NULL,
PRIMARY KEY (ID)
);
CREATE TABLE BAR (
ID INTEGER NOT NULL,
PRIMARY KEY (ID)
);
CREATE TABLE SNAFU (
ID INTEGER NOT NULL,
PRIMARY KEY (ID)
);
CREATE TABLE FOO_BAR_SNAFU (
FOO_ID INTEGER NOT NULL,
BAR_ID INTEGER NOT NULL,
SNAFU_ID INTEGER NOT NULL,
PRIMARY KEY (FOO_ID, BAR_ID, SNAFU_ID)
);
ALTER TABLE FOO_BAR_SNAFU ADD CONSTRAINT FK_FOO FOREIGN KEY (FOO_ID) REFERENCES FOO(ID);
ALTER TABLE FOO_BAR_SNAFU ADD CONSTRAINT FK_BAR FOREIGN KEY (BAR_ID) REFERENCES BAR(ID);
ALTER TABLE FOO_BAR_SNAFU ADD CONSTRAINT FK_SNAFU FOREIGN KEY (SNAFU_ID) REFERENCES SNAFU(ID);
このように3つのエンティティテーブルと1つの関連テーブルからなる関連をTernary Associationと呼びます。
そして、"Ternary Associations" mappingとは、これらのテーブルを3つのモデルクラス(Foo, Bar, Snafu)に関連も含めてマッピングすることを言います.
■マッピング方法
Ternary Associationをマッピングするには2つの方法があります.
(1)composite elementを使う方法
これは次のようなクラスにマッピングする方法です.
この方法ついての説明・サンプルは、http://www.xylax.net/hibernate/ternary.html にあります。
(2)index-many-to-many(Entity Map)を使う方法
これは次のようなクラスにマッピングする方法です。
関連をMapを使って実現しているため、次のような問題があると思います.
・FOO_BAR_SNAFUテーブルに(FOO_ID, BAR_ID, SNAFU_ID) = (1, 10, 111), (1, 10, 222)といったレコードを登録しようとした場合、
MapのキーとなるBarオブジェクトが一致すると(Bar#equalsメソッドによって判定される)、どちらか一方のレコードしか登録されない.
・FOO_BAR_SNAFUテーブルに(FOO_ID, BAR_ID, SNAFU_ID) = (1, 10, 111), (1, 10, 222)といったレコードが登録されている場合、
Session.find("from Foo")を実行した時、この2つのレコードがMapに反映されるのか?それともどちらかのレコードしかMapに反映されないのか?
こんな問題がありそうなindex-many-to-many方式ですが、あえてこのマッピング方法についてXDocletタグの書き方や生成されたマッピングファイルについて見ていくことにします。
所詮日記のネタですから :)
■モデルクラス & XDocletタグ & マッピングファイル
Fooクラスのソース&XDocletタグは次のような感じになります.
[Foo.java] package fumi.hibernate.tutorial.ternary; import java.util.Map; import fumi.hibernate.core.model.BaseObject; /** * Ternary Associations mappingをindex-many-to-many(Entity Map)を * 使って実現する. * * @hibernate.class table="FOO" */ public class Foo extends BaseObject { private Integer id; private Map map; public Foo() {} /** * @hibernate.id * column="ID" * generator-class="assigned" * unsaved-value="null" */ public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } /** * @hibernate.map * table="FOO_BAR_SNAFU" * * @hibernate.collection-key * column="FOO_ID" * * @hibernate.index-many-to-many * column="BAR_ID" * class="fumi.hibernate.tutorial.ternary.Bar" * * @hibernate.collection-many-to-many * column="SNAFU_ID" * class="fumi.hibernate.tutorial.ternary.Snafu" */ public Map getBarSnafuMap() { return map; } public void setBarSnafuMap(Map map) { this.map = map; } }
コレクションに対するXDocletタグの記述は、Entity Mapで説明したものと基本的に同じですが、繰り返し記述しておきます.
コレクションのタイプがMapなのでこのタグを使ってコレクション・プロパティの使用を宣言します.
table属性には関連テーブルの名前を設定します.
次にこのタグのcolumn属性で、コレクションの所有者であるFooクラスに対応するテーブル(FOOテーブル)への関連テーブルのFKカラム名を指定します(ながっ).
コレクションを指定する場合、このタグは必須です.
・@hibernate.index-many-to-many
Map, List, 配列のようなindexedなコレクションの場合、indexについてのマッピングを指定する必要があります.
indexのマッピングの指定には通常<index>タグ(@hibernate.collection-index)が使用されますが、今回のようにindexすなわちMapのキーがエンティティオブジェクトの場合は<index-many-to-many>タグ(@hibernate.index-many-to-many)を指定します。
このタグのcolumn属性には、index情報を格納するテーブル(BARテーブル)へのFKカラム名を指定します.class属性には、indexとなるクラスを指定します。
・@hibernate.collection-many-to-many
最後にコレクション(MapのValue)に格納する値について指定する必要があります。
このケースでは、FOOテーブルとコレクションに格納する値を保持しているSNAFUテーブルの間にはmany-to-manyの関連がありますので、@hibernate.many-to-manyタグを使用します.
column属性にはコレクションの値を格納しているテーブル(SNAFUテーブル)への関連テーブルのFKカラム名、class属性にはコレクションの値を表現するクラス名を指定します.
Barクラス、Snafuクラスのソース&XDocletタグは次のようになります.これらは1つのプロパティしか含まないとてもシンプルなPOJOクラスです.
[Bar.java] package fumi.hibernate.tutorial.ternary; import fumi.hibernate.core.model.BaseObject; /** * @hibernate.class table="BAR" */ public class Bar extends BaseObject { private Integer id; public Bar() {} public Bar(Integer id) { this.id = id; } /** * @hibernate.id * column="ID" * generator-class="assigned" * unsaved-value="null" */ public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } /** * BarクラスはMapのキーとなるので、必要ならばequals(), hashCode()メソッドを * オーバーライドし、オブジェクトの同一性について定義する. * ここではコメントアウトしている. */ /* public boolean equals(Object obj) { if (! (obj instanceof Bar)) { return false; } Bar other = (Bar) obj; return this.getId().equals(other.getId()); } public int hashCode() { return id.hashCode(); }*/ }
[Snafu.java] package fumi.hibernate.tutorial.ternary; import fumi.hibernate.core.model.BaseObject; /** * @hibernate.class table="SNAFU" */ public class Snafu extends BaseObject { private Integer id; public Snafu() {} public Snafu(Integer id ) { this.id = id; } /** * @hibernate.id * column="ID" * generator-class="assigned" * unsaved-value="null" */ public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } }
XDocletによって生成されたマッピングファイルは次のようになりました(不要なコメントは削除).
[Foo.hbm.xml] <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="fumi.hibernate.tutorial.ternary.Foo" table="FOO" dynamic-update="false" dynamic-insert="false" > <id name="id" column="ID" type="java.lang.Integer" unsaved-value="null" > <generator class="assigned"> </generator> </id> <map name="barSnafuMap" table="FOO_BAR_SNAFU" lazy="false" sort="unsorted" inverse="false" cascade="none" > <key column="FOO_ID" > </key> <index-many-to-many class="fumi.hibernate.tutorial.ternary.Bar" column="BAR_ID" /> <many-to-many class="fumi.hibernate.tutorial.ternary.Snafu" column="SNAFU_ID" outer-join="auto" /> </map> </class> </hibernate-mapping>
[Bar.hbm.xml] <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="fumi.hibernate.tutorial.ternary.Bar" table="BAR" dynamic-update="false" dynamic-insert="false" > <id name="id" column="ID" type="java.lang.Integer" unsaved-value="null" > <generator class="assigned"> </generator> </id> </class> </hibernate-mapping>
[Snafu.hbm.xml] <?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="fumi.hibernate.tutorial.ternary.Snafu" table="SNAFU" dynamic-update="false" dynamic-insert="false" > <id name="id" column="ID" type="java.lang.Integer" unsaved-value="null" > <generator class="assigned"> </generator> </id> </class> </hibernate-mapping>
■DAO実装例
次にDAOの実装例を示します。
今回はマッピング方法について興味があるため、DAOの実装はSpring FrameworkのHibernateサポート機能(HibernateTemplate)を利用して横着をしています。
[FooDAOHibernate.java] package fumi.hibernate.tutorial.ternary; import java.util.List; import org.springframework.orm.hibernate.HibernateTemplate; import org.springframework.orm.hibernate.support.HibernateDaoSupport; public class FooDAOHibernate extends HibernateDaoSupport implements FooDAO { public List listFoos() { return getHibernateTemplate().find("from Foo f order by f.id"); } public Foo getFoo(Integer id) { return (Foo) getHibernateTemplate().load(Foo.class, id); } public Foo createFoo(Foo foo) { Integer id = (Integer) getHibernateTemplate().save(foo); return getFoo(id); } public void updateFoo(Foo foo) { getHibernateTemplate().update(foo); } public void deleteFoo(Integer id) { Foo foo = getFoo(id); getHibernateTemplate().delete(foo); } // for Bar public Bar getBar(Integer id) { return (Bar) getHibernateTemplate().load(Bar.class, id); } public Bar createBar(Bar bar) { Integer id = (Integer) getHibernateTemplate().save(bar); return getBar(id); } // for Snafu public Snafu getSnafu(Integer id) { return (Snafu) getHibernateTemplate().load(Snafu.class, id); } public Snafu createSnafu(Snafu snafu) { Integer id = (Integer) getHibernateTemplate().save(snafu); return getSnafu(id); } }
■メインクラス
以下はメインクラス(抜粋)です。
Foo foo; Bar bar; Snafu snafu; Map map; // 登録処理 foo = new Foo(); foo.setId(new Integer(1)); map = new HashMap(); bar = new Bar(new Integer(10)); dao.createBar(bar); snafu = new Snafu(new Integer(100)); dao.createSnafu(snafu); map.put(bar, snafu); bar = new Bar(new Integer(11)); dao.createBar(bar); snafu = new Snafu(new Integer(101)); dao.createSnafu(snafu); map.put(bar, snafu); bar = new Bar(new Integer(12)); dao.createBar(bar); snafu = new Snafu(new Integer(102)); dao.createSnafu(snafu); map.put(bar, snafu); foo.setBarSnafuMap(map); dao.createFoo(foo); // 登録したデータをデータベースから読み込む foo = dao.getFoo(new Integer(1)); log.debug(foo); // Mapには関連するBar, Snafuが入っている
実行後のテーブルには次のようにレコードが登録されます.
・FOO (ID)=(1) ・BAR (ID)=(10), (11), (12) ・SNAFU (ID)=(100), (101), (102) ・FOO_BAR_SNAFU (FOO_ID, BAR_ID, SNAFU_ID)=(1, 10, 100), (1, 11, 101), (1, 12, 102)
■Memo
・Mapプロパティへの操作(put, remove等)をした後に、Fooオブジェクトをupdateすることで変更された関連情報が関連テーブルに反映されます。
・関連情報(Map)を保持しているFooオブジェクトを削除するとFOOテーブルと関連テーブルの該当レコードが削除されます。このとき、BAR, SNAFUテーブルのレコードは削除されません。
・「FOO_BAR_SNAFUテーブルに(FOO_ID, BAR_ID, SNAFU_ID) = (1, 10, 111), (1, 10, 222)といったレコードを登録しようとした場合、MapのキーとなるBarオブジェクトが一致すると(Bar#equalsメソッドによって判定される)、どちらか一方のレコードしか登録されない。」ことを確認しました。
・「FOO_BAR_SNAFUテーブルに(FOO_ID, BAR_ID, SNAFU_ID) = (1, 10, 111), (1, 10, 222)といったレコードが登録されている場合、Session.find("from Foo")を実行した時、この2つのレコードがMapに反映されるのか?それともどちらかのレコードしかMapに反映されないのか?」1つのレコードしかMapには格納されませんでした.これは、上記Barクラスのソースのequalsメソッドを有効にしても同じ結果でした.
ということで、Ternary Associationsのマッピングにはindex-many-to-many方式よりもcomposite-element方式を採用した方が良いように思います。それとも改善する余地があるのかな?
ということでおしまいです。
米持さんの連載がはじまるらしい.
*1:勉強中なので誤った記述が多々あると思いますが、お許しを。日記ですから^^; ご指摘いただけると助かります.