Hibernate入門記 コレクションその2 set で many-to-many しかも双方向

前回に引き続いて <set> を扱いますが,今回は多対多に挑戦です.ついでに双方向も.
ということで,「6.3. Collections of Values and Many-To-Many Associations」とか「6.8. Bidirectional Associations」あたりの学習です.
Hibernateは,多対多関連も1対多関連と同じように,コレクションとして扱います.そのための指定をするのが

  • <set> 要素

なのですが,これは前回学習済み.問題ありません.たぶん.
その内容としては,やはり one-to-many と同様に

  • <key> 要素

を記述します.これも前回学習済み.
それに続けて,<one-to-many> 要素の代わりに

  • <meny-to-many> 要素

を使って関連のマッピングを指定します.
この要素は,次の属性を持っています.

class
コレクションの要素となるエンティティの型を指定します.
column
外部キーのカラム名です.リファレンスでは必須となっていますが,DTDでは任意です.どっち?
outer-join
外部結合を使用する場合に指定します.

DTDには他にも forign-key という属性もあるようです.column のレガシーな名前?
うーん,<many-to-many> 要素の column 属性と,<key> 要素の column 属性の関係は?
多対多の関連を表現するには,関連テーブルを使用します.関連テーブルは,二つのテーブルに対する外部キーを持ちます.そこで,

  • こっち(自分)への外部キーを <key> 要素の column 属性で指定する.
  • あっち(相手)への外部キーを <many-to-many> 要素の column 属性で指定する.

ということのようです.
ふむふむ.1対多の場合もやはり,こっちへの外部キーを含むテーブル(関連先のエンティティそのもの)を <key> 要素の column 属性で指定しました.なるほど,一貫していますね.
おっと,関連エンティティのテーブルはどこで指定するのでしょうか? どうやら,

  • <set> 要素の table 属性で指定する.

みたいですね.省略した場合はコレクションのプロパティ名が使われます.らじゃ.
多対多関連のマッピングについては,これくらいでどうにかなりそうです.


では,続いて双方向の関連について学習します.
まず,双方向の関連は1対多・多対多で使うことができるようです.ただし,多の側を参照するために使うことができるコレクションは,

  • セット
  • バッグ

の二つだけで,マップやリスト,配列はサポートされないとのこと.今はセットの学習中なので問題ありません.
そして,実際に双方向関連を使う場合に必要なのは,1対多の場合は多の側,多対多の場合は任意の一方の <set> 要素で inverse 属性に true を指定するだけみたいです.
注意が必要なのは,多対多で inverse 属性に true を指定するのは,関連づけられる二つの永続クラスのどちらか一方だけ,というところですね.
どちらの永続クラスの inverse 属性に true を指定したかによって,関連テーブルの更新に影響があるようです.
仮にAとBという二つの永続クラスが多対多の関連を持ち,Aの inverse 属性に true を指定した場合,Bを save() しても関連テーブルはINSERT/UPDATEされないみたい.Aを save() した場合のみ,関連テーブルが INSERT/UPDATE されるらしいのです.うーみゅ,これは注意が必要かも.
1対多の場合は,1の側にしか inverse 属性を指定できませんから,1の側の永続クラスを save() した場合にのみ外部キーが更新されるということでしょう.この場合,外部キーは多の側のテーブルにあるわけですから,1の側のテーブルをINSERT/UPDATEするときに多の側のテーブルをUPDATEするという動きになると思われます.うーん,効率的といえるのか疑問...


ともあれ,必要な情報はそろった気がするのでお試ししましょう.
モデルと多対多の関係にあるものってなんでしょうか? とりあえず,掲載されている雑誌でしょうか.新鮮味に欠けるネタですが,それでやってみますか.
ということで,テーブルを用意します.

CREATE TABLE MODEL (
    ID INTEGER IDENTITY,
    NAME VARCHAR
)

CREATE TABLE MAGAZINE (
    ID INTEGER IDENTITY,
    NAME VARCHAR
)

CREATE TABLE MODEL_MAGAZINE (
    MODEL INTEGER,
    MAGAZINE
)

え? 外部キー制約を付けないのかって? いやその,ずっと忘れていたのです,あうあう.だからこれからもつけません! 心より恥じる.
次行きますよっ! 次,次!!(エビちゃん風)
モデルのクラスです.

package study;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import net.sf.hibernate.CallbackException;
import net.sf.hibernate.Lifecycle;
import net.sf.hibernate.Session;

public class Model implements Lifecycle {
    int id = -1;
    String name;
    Set magazine;

    public Model() {
    }
    public Model(String name) {
        this.name = name;
        this.magazine = new HashSet();
    }

    public String toString() {
        StringBuffer buf = new StringBuffer();
        buf.append(name).append(" [");
        Iterator it = magazine.iterator();
        while (it.hasNext()) {
            Magazine magazine = (Magazine) it.next();
            buf.append(magazine.name).append(", ");
        }
        buf.setLength(buf.length() - 2);
        buf.append("]");
        return new String(buf);
    }

    public void onLoad(Session s, Serializable id) {
        System.out.println("onLoad() : " + toString());
    }
    public boolean onSave(Session s) throws CallbackException {
        System.out.println("onSave() : " + toString());
        return false;
    }
    public boolean onUpdate(Session s) throws CallbackException {
        System.out.println("onUpdate() : " + toString());
        return false;
    }
    public boolean onDelete(Session s) throws CallbackException {
        System.out.println("onDelete() : " + toString());
        return false;
    }
}

モデルのマッピングファイル.

<?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 auto-import="false" package="study">
    <class name="Model">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>

        <property name="name" access="field"/>
        <set name="magazine" access="field" inverse="true" table="MODEL_MAGAZINE">
            <key column="model"/>
            <many-to-many class="Magazine" column="magazine"/>
        </set>
    </class>
</hibernate-mapping>

そして雑誌のクラス.

package study;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import net.sf.hibernate.CallbackException;
import net.sf.hibernate.Lifecycle;
import net.sf.hibernate.Session;

public class Magazine implements Lifecycle {
    int id = -1;
    String name;
    Set model;

    public Magazine() {
    }
    public Magazine(String name) {
        this.name = name;
        this.model = new HashSet();
    }

    public String toString() {
        StringBuffer buf = new StringBuffer();
        buf.append(name).append(" [");
        Iterator it = model.iterator();
        while (it.hasNext()) {
            Model model = (Model) it.next();
            buf.append(model.name).append(", ");
        }
        buf.setLength(buf.length() - 2);
        buf.append("]");
        return new String(buf);
    }

    public void onLoad(Session s, Serializable id) {
        System.out.println("onLoad() : " + toString());
    }
    public boolean onSave(Session s) throws CallbackException {
        System.out.println("onSave() : " + toString());
        return false;
    }
    public boolean onUpdate(Session s) throws CallbackException {
        System.out.println("onUpdate() : " + toString());
        return false;
    }
    public boolean onDelete(Session s) throws CallbackException {
        System.out.println("onDelete() : " + toString());
        return false;
    }
}

雑誌のマッピングファイル.

<?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 auto-import="false" package="study">
    <class name="Magazine">
        <id name="id" access="field" unsaved-value="-1">
            <generator class="identity"/>
        </id>

        <property name="name" access="field"/>
        <set name="model" access="field" table="MODEL_MAGAZINE">
            <key column="magazine"/>
            <many-to-many class="Model" column="model"/>
        </set>
    </class>
</hibernate-mapping>

あと,hibernate.cfg.xml で読み込むマッピングファイルについて,前回の study/Cm.hbm.xml を study/Magazine.hbm.xml に変更しておきます.
そして実行用のクラス.

package study;
import java.util.Iterator;
import net.sf.hibernate.Session;
import net.sf.hibernate.SessionFactory;
import net.sf.hibernate.cfg.Configuration;

public class Main {
    public static void main(String[] args) {
        try {
            Configuration config = new Configuration();
            SessionFactory factory = config.configure().buildSessionFactory();

            Session session = factory.openSession();

            Magazine cancam = new Magazine("CanCam");
            Magazine vivi = new Magazine("ViVi");
            Magazine classy = new Magazine("CLASSY");

            Model yuri = new Model("蛯原友里");
            yuri.magazine.add(cancam);
            cancam.model.add(yuri);

            Model sayo = new Model("相沢紗世");
            sayo.magazine.add(vivi);
            vivi.model.add(sayo);
            sayo.magazine.add(classy);
            classy.model.add(sayo);

            Model yu = new Model("山田優");
            yu.magazine.add(cancam);
            cancam.model.add(yu);

            session.save(cancam);
            session.save(vivi);
            session.save(classy);
            session.save(yuri);
            session.save(sayo);
            session.save(yu);

            session.flush();

            session = factory.openSession();
            Iterator it = session.find("from study.Model").iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }
            it = session.find("from study.Magazine").iterator();
            while (it.hasNext()) {
                System.out.println(it.next());
            }
        }
        catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

CLASSYみたいな上流階級な雑誌は読んでいないので,紗世ちゃんが今も登場しているのか知らないのですが,とりあえずということで.
こいつを実行!!

 onSave() : CanCam [山田優, 蛯原友里]
 Hibernate: insert into Magazine (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 onSave() : ViVi [相沢紗世]
 Hibernate: insert into Magazine (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 onSave() : CLASSY [相沢紗世]
 Hibernate: insert into Magazine (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 onSave() : 蛯原友里 [CanCam]
 Hibernate: insert into Model (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 onSave() : 相沢紗世 [CLASSY, ViVi]
 Hibernate: insert into Model (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 onSave() : 山田優 [CanCam]
 Hibernate: insert into Model (name, id) values (?, null)
 Hibernate: CALL IDENTITY()
 Hibernate: insert into MODEL_MAGAZINE (magazine, model) values (?, ?)
 Hibernate: select model0_.id as id, model0_.name as name from Model model0_
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 Hibernate: select model0_.model as model__, model0_.magazine as magazine__ from MODEL_MAGAZINE model0_ where model0_.magazine=?
 onLoad() : CanCam [null, 蛯原友里]
 onLoad() : 蛯原友里 [CanCam]
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 Hibernate: select model0_.model as model__, model0_.magazine as magazine__ from MODEL_MAGAZINE model0_ where model0_.magazine=?
 onLoad() : ViVi [相沢紗世]
 Hibernate: select magazine0_.id as id0_, magazine0_.name as name0_ from Magazine magazine0_ where magazine0_.id=?
 Hibernate: select model0_.model as model__, model0_.magazine as magazine__ from MODEL_MAGAZINE model0_ where model0_.magazine=?
 onLoad() : CLASSY [相沢紗世]
 onLoad() : 相沢紗世 [CLASSY, ViVi]
 Hibernate: select magazine0_.magazine as magazine__, magazine0_.model as model__ from MODEL_MAGAZINE magazine0_ where magazine0_.model=?
 onLoad() : 山田優 [CanCam]
 蛯原友里 [CanCam]
 相沢紗世 [CLASSY, ViVi]
 山田優 [CanCam]
 Hibernate: select magazine0_.id as id, magazine0_.name as name from Magazine magazine0_
 CanCam [山田優, 蛯原友里]
 ViVi [相沢紗世]
 CLASSY [相沢紗世]

\(^o^)/
動きを見ていると,Session#flush() を呼び出すと関連テーブル(MODEL_MAGAZINE)へのINSERTが行われるようです.
SELECTがずいぶんたくさん発行されてるのが気になりますね... こういうもの? このあたりはいずれ「14. Improving performance」で解説されると期待して,今は忘れることにしますか.


なんか吐血しないと入門した気がしないわけですが,今日はこれくらいでお終いにします.
心より恥じる. ←吐血していないのになぜ恥じる?>自分