Top > MyPage
 

hibernate外部キーの双方向一対一関連

双方向一対一関連

Hibernateの外部キーの双方向一対一関連の使い方について、実際に簡単なテーブルを用意して確認したのでまとめる。

今回は、検証のためのアプリをmavenを用いて作ったが、mavenのプロジェクトの作り方については割愛する。

なお、検証にはこの時点のhibernateの最新バージョン(3.3.1.GA)をもちいた。

この検証を行うにあたって、HIBERNATE リファレンスガイドを参考にした。

テーブル構成

以下に、実際に使用したDBのテーブル構成のER図を示す。

ER図

図の様に、今回はSTAFFとSTAFF_INFOを用意した。

STAFFとSTAFF_INFOのプライマリキーは、それぞれとである。どちらもシーケンスで生成する。そして、STAFF_INFOは外部キーとしてSTAFF_IDを持つ。

以下に、各テーブルのCREATE文を示す。STAFF_INFOのCONSTRAINTがコメントアウトされているのは、検証をするうえで楽にするためであり、こうした方がよいというわけではない。

CREATE TABLE STAFF (
       STAFF_ID SERIAL NOT NULL
     , STAFFNAME VARCHAR(32)
     , PASSWORD VARCHAR(64)
     , PRIMARY KEY (STAFF_ID)
);

CREATE TABLE STAFF_INFO (
       ID SERIAL NOT NULL
     , STAFF_ID INTEGER
     , AGE INTEGER
     , SEX INTEGER
     , PRIMARY KEY (ID)
--     , CONSTRAINT FK_STAFF_INFO_1 FOREIGN KEY (STAFF_ID)
--                  REFERENCES STAFF (STAFF_ID)
);

DTO構成

DTOは、STAFFテーブルとSTAFF_INFOテーブルのそれぞれに対応するStaffとStaffInfoを作成した。

それぞれのクラスの持つ変数は次のようにした。(メソッドは省略)お互いが、相手のクラスの変数をもつことがわかる。

public class Staff implements Serializable {

    private Integer staffId;

    private String staffname;

    private String password;
    
    private StaffInfo staffInfo;

}




public class StaffInfo implements Serializable {

    private Integer id;

    private Integer age;

    private Integer sex;
    
    private Staff staff;


}

このときのマッピング定義は、それぞれ次のようにした。

<hibernate-mapping>
  <class table="STAFF" name="com.chikkun.common.login.db.Staff">
    <id unsaved-value="null" name="staffId" type="java.lang.Integer" column="STAFF_ID">
      <generator class="sequence">
        <param name="sequence">STAFF_STAFF_ID_SEQ</param>
      </generator>
    </id>
    <property name="staffname" column="STAFFNAME"/>
    <property name="password" column="PASSWORD"/>
    <one-to-one name="staffInfo" class="com.chikkun.common.login.db.StaffInfo"/>
  </class>
</hibernate-mapping>


<hibernate-mapping>
  <class table="STAFF_INFO" name="com.chikkun.common.login.db.StaffInfo">
    <id unsaved-value="null" name="id" type="java.lang.Integer" column="ID">
      <generator class="sequence">
        <param name="sequence">STAFF_INFO_ID_SEQ</param>
      </generator>
    </id>
    <property name="age" column="AGE"/>
    <property name="sex" column="SEX"/>
    <many-to-one unique="true" column="STAFF_ID" class="com.chikkun.common.login.db.Staff" name="staff"/>
  </class>
</hibernate-mapping>

ここで、注目してほしいのはStaffInfoクラスのマッピングにあるmany-to-oneタグに設定されているである。一対一であるので、必ずunique="true"で設定する。columnは通常のmany-to-oneと同じで外部キーのカラム名である。

検証をわかりやすくするため、cascadeは設定していない。また、Staffと関連を持たないStaffInfoも保存できるようにSTAFF_IDはnullを許している。

動作結果の検証 (cascade無し)

cascadeを設定していないので、Staff、StaffInfoを関連付けてから、片方だけsaveしても意味がない。そういった、自明のことは省略する。

単純にinsertする

まず、両方のテーブルにデータを挿入する例を挙げる。

    // generalDAOという汎用DAOクラスを使うということにする。generalDAOのソースは後述
    
    //新しいStaffインスタンス
    Staff staff = new Staff();
    staff.setStaffname("test1");
    staff.setPassword("pass1");
    //StaffInfoに何もセットしないのは、わざとである
    
    StaffInfo info = new StaffInfo();
    info.setAge(new Integer(1));
    info.setSex(new Integer(1));
    //こちらにはStaffをセット
    info.setStaff(staff);
    
    //staffが先。
    generalDAO.saveStaff(staff);
    //これで、staffは分離オブジェクトになったのでstaffIdに値が入っている。
    
    //StaffInfoを保存
    generalDAO.saveStaffInfo(info);

すると、処理後のテーブルの内容は次のようになる

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1

順当な結果である。

次に、StaffにstaffInfoをセットして、StaffInfoにStaffをセットしない場合を見てみよう。

    
    //新しいStaffインスタンス
    Staff staff = new Staff();
    staff.setStaffname("test2");
    staff.setPassword("pass2");
    
    
    StaffInfo info = new StaffInfo();
    info.setAge(new Integer(1));
    info.setSex(new Integer(1));
    //今度は、こちらにはStaffをセットしない
    //StaffにStaffInfoをセットする
    staff.setStaffInfo(info);
    
    //staffが先。
    generalDAO.saveStaff(staff);
    //これで、staffは分離オブジェクトになったのでstaffIdに値が入っている。
    
    //StaffInfoを保存
    generalDAO.saveStaffInfo(info);

今度は、処理後のテーブルの内容は次のようになる

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1

この結果から

  • 関連の管理は外部キーを持つテーブルに対応したDTOの方が責任を持つ。
  • 外部キーの双方向一対一関連(many-to-oneとone-to-one)は関連の無いレコードを作ることが出来る。

ということがわかる。

自明のことではあるが、念のため。上の例では、必ずStaffを先に保存している。逆にStaffInfoを先に保存したとすると、そのStaffInfoにStaffがセットされていない場合は外部キーがnullになるだけなのだが、一時オブジェクトであるStaffをセットしていた場合はエラーとなる。具体的には次のようなコードを実行した場合である。

    
    //新しいStaffインスタンス
    Staff staff = new Staff();
    staff.setStaffname("hoge");
    staff.setPassword("huga");
    //StaffInfoに何もセットしないのは、わざとである
    
    StaffInfo info = new StaffInfo();
    info.setAge(new Integer(0));
    info.setSex(new Integer(0));
    //こちらにはStaffをセット
    info.setStaff(staff);
        
    //先にStaffInfoを保存。ここでRuntimeExceptionが発生する。
    generalDAO.saveStaffInfo(info);
    
    //次にStaffを保存  ここまでこない。
    generalDAO.saveStaff(staff);

update

deleteは、cascadeを指定していないと、削除指定したオブジェクトに対応するレコードが削除されるだけなので、省略。

同じ理由で、Staffのアップデートも省略。StaffInfoのupdateを行う。

まず、DBの状態は次のようになっているとする。

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1

STAFF_INFOテーブルのID=2のレコードが関連を持っていないので、関連を持たせてアップデートすることにする。

エラーになることを期待して、STAFF_ID=1のレコードと関連を持たせるようなコードを実行してみる。

    
    //IDが2のStaffInfoを取得
    StaffInfo info = generalDAO.findStaffInfoById(new Integer(2));
    
    //staffIdが1のStaffを取得。
    Staff staff = generalDAO.findStaffById(new Integer(1));
    
    //すでに他と関連を持つstaffをセット
    info.setStaff(staff);
    
    //StaffInfoをアップデート
    generalDAO.updateStaffInfo(info);

実行した結果、何のエラーも発生することなくアップデートが完了して、DBは次のようになった。

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1 1

このように齟齬のあるデータになってしまったため、DB的にはエラーにならなくとも、システム的には完全にエラーである。

このことから

  • 外部キーのカラムには、安全のため、外部キー制約とユニーク制約をつける必要あり

ということがわかる。

ここからは、制約を付けた状態で検証する。

CREATE TABLE STAFF (
       STAFF_ID SERIAL NOT NULL
     , STAFFNAME VARCHAR(32)
     , PASSWORD VARCHAR(64)
     , PRIMARY KEY (STAFF_ID)
);

CREATE TABLE STAFF_INFO (
       ID SERIAL NOT NULL
     , STAFF_ID INTEGER UNIQUE
     , AGE INTEGER
     , SEX INTEGER
     , PRIMARY KEY (ID)
     , CONSTRAINT FK_STAFF_INFO_1 FOREIGN KEY (STAFF_ID)
                  REFERENCES STAFF (STAFF_ID)
);

この場合でも、STAFF_IDがnullである状態は許されるので、関連を持たないレコードを作ることは出来る。

ここでまた、先ほどと同じ状態で、齟齬が発生するようなコードを実行してみたところConstraintViolationExceptionが発生した。

動作結果の検証 (StaffInfo側からcascade all)

StaffからStaffInfoへという向きのカスケードは、まったく意外性もないのであとで。StaffInfoからStaffへというカスケードを設定する。

そのマッピングを次に示す。

<hibernate-mapping>
  <class table="STAFF" name="com.chikkun.common.login.db.Staff">
    <id unsaved-value="null" name="staffId" type="java.lang.Integer" column="STAFF_ID">
      <generator class="sequence">
        <param name="sequence">STAFF_STAFF_ID_SEQ</param>
      </generator>
    </id>
    <property name="staffname" column="STAFFNAME"/>
    <property name="password" column="PASSWORD"/>
    <one-to-one name="staffInfo" class="com.chikkun.common.login.db.StaffInfo"/>
  </class>
</hibernate-mapping>


<hibernate-mapping>
  <class table="STAFF_INFO" name="com.chikkun.common.login.db.StaffInfo">
    <id unsaved-value="null" name="id" type="java.lang.Integer" column="ID">
      <generator class="sequence">
        <param name="sequence">STAFF_INFO_ID_SEQ</param>
      </generator>
    </id>
    <property name="age" column="AGE"/>
    <property name="sex" column="SEX"/>
    <many-to-one unique="true" column="STAFF_ID" cascade="all" class="com.chikkun.common.login.db.Staff" name="staff"/>
  </class>
</hibernate-mapping>

単純なインサートを行う

先の例では、必ずStaffを先に保存するようにしていたが、今度はStaffInfoから保存する。

まず、DBは以下の状態であった。

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1

このときに、次のようなコードを実行した。

    
    //新しいStaffインスタンス
    Staff staff = new Staff();
    staff.setStaffname("test3");
    staff.setPassword("pass3");
    //StaffInfoに何もセットしないのは、わざとである
    
    StaffInfo info = new StaffInfo();
    info.setAge(new Integer(1));
    info.setSex(new Integer(1));
    //こちらにはStaffをセット
    info.setStaff(staff);
    
        
    //StaffInfoを保存 カスケードを設定しているのでStaffを保存してからこちらが保存されるはず。
    generalDAO.saveStaffInfo(info);

結果、予想通り、問題なく保存された。

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
3 test3 pass3
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1
3 3 1 1

この結果から、delete、updateについても結果が想像できるだろう。

動作結果の検証 (Staff側からcascade all)

StaffからStaffInfoへという向きのカスケードを設定する。

そのマッピングを次に示す。

<hibernate-mapping>
  <class table="STAFF" name="com.chikkun.common.login.db.Staff">
    <id unsaved-value="null" name="staffId" type="java.lang.Integer" column="STAFF_ID">
      <generator class="sequence">
        <param name="sequence">STAFF_STAFF_ID_SEQ</param>
      </generator>
    </id>
    <property name="staffname" column="STAFFNAME"/>
    <property name="password" column="PASSWORD"/>
    <one-to-one name="staffInfo" class="com.chikkun.common.login.db.StaffInfo"cascade="all"/>
  </class>
</hibernate-mapping>


<hibernate-mapping>
  <class table="STAFF_INFO" name="com.chikkun.common.login.db.StaffInfo">
    <id unsaved-value="null" name="id" type="java.lang.Integer" column="ID">
      <generator class="sequence">
        <param name="sequence">STAFF_INFO_ID_SEQ</param>
      </generator>
    </id>
    <property name="age" column="AGE"/>
    <property name="sex" column="SEX"/>
    <many-to-one unique="true" column="STAFF_ID" class="com.chikkun.common.login.db.Staff" name="staff"/>
  </class>
</hibernate-mapping>

単純なインサートを行う

まず、DBは以下の状態であった。

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
3 test3 pass3
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1
3 3 1 1

このときに、次のようなコードを実行した。

    
    //新しいStaffインスタンス
    Staff staff = new Staff();
    staff.setStaffname("test4");
    staff.setPassword("pass4");
    
    
    StaffInfo info = new StaffInfo();
    info.setAge(new Integer(1));
    info.setSex(new Integer(1));
    
    //Staff→StaffInfoとカスケードさせるため
    //StaffにStaffInfoをセットする
    staff.setStaffInfo(info);
    
    //StaffInfoに何もセットしないのは、わざとである
    //こちらにStaffをセットしないので、外部キーのカラムがnullで保存されるはず
    //info.setStaff(staff);
    
    
        
    //Staffを保存 カスケードを設定しているのでStaffInfoも保存される。
    generalDAO.saveStaff(staff);
    

結果、つぎのようなSQLが発行された。

    
     Hibernate: select nextval ('STAFF_STAFF_ID_SEQ')
     Hibernate: select nextval ('STAFF_INFO_ID_SEQ')
     Hibernate: insert into STAFF (STAFFNAME, PASSWORD, STAFF_ID) values (?, ?, ?)
     Hibernate: insert into STAFF_INFO (AGE, SEX, STAFF_ID, ID) values (?, ?, ?, ?)



その結果、DBの内容は予想通りになった。
STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
3 test3 pass3
4 test4 pass4
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1
3 3 1 1
4 1 1

分離オブジェクトをsaveするとどうなるか

まず、DBは以下の状態であった。

STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
3 test3 pass3
4 test4 pass4
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1
3 3 1 1
4 1 1

このときに、次のようなコードを実行した。

    
    //新しいStaffインスタンス
    Staff staff = new Staff();
    staff.setStaffname("test5");
    staff.setPassword("pass5");
    
    
    StaffInfo info = new StaffInfo();
    info.setAge(new Integer(1));
    info.setSex(new Integer(1));
    
    //Staff→StaffInfoとカスケードさせるため
    //StaffにStaffInfoをセットする
    staff.setStaffInfo(info);
    
    //StaffInfoに何もセットしないのは、わざとである
    //こちらにStaffをセットしないので、外部キーのカラムがnullで保存されるはず
    //info.setStaff(staff);
    
    
        
    //Staffを保存 カスケードを設定しているのでStaffInfoも保存される。
    generalDAO.saveStaff(staff);
    
    //分離オブジェクトになったStaffInfoを保存しようとするとどうなるか
    generalDAO.saveStaffInfo(info);

結果、つぎのようなSQLが発行された。分離オブジェクトでも、saveすると新しいidが割り振られてinsertされている。

    
     Hibernate: select nextval ('STAFF_STAFF_ID_SEQ')
     Hibernate: select nextval ('STAFF_INFO_ID_SEQ')
     Hibernate: insert into STAFF (STAFFNAME, PASSWORD, STAFF_ID) values (?, ?, ?)
     Hibernate: insert into STAFF_INFO (AGE, SEX, STAFF_ID, ID) values (?, ?, ?, ?)
     Hibernate: select nextval ('STAFF_INFO_ID_SEQ')
     Hibernate: insert into STAFF_INFO (AGE, SEX, STAFF_ID, ID) values (?, ?, ?, ?)



その結果、DBの内容は次のようになった。
STAFF
STAFF_ID STAFFNAME PASSWORD
1 test1 pass1
2 test2 pass2
3 test3 pass3
4 test4 pass4
5 test5 pass5
STAFF_INFO
ID STAFF_ID AGE SEX
1 1 1 1
2 1 1
3 3 1 1
4 1 1
5 1 1
6 1 1

ここで、STAFF_INFOテーブルの制約を思い出してほしい。このテーブルのSTAFF_IDにはユニーク制約があるため、値の重複は許されない。

であるので、上のようにStaffInfoにStaffがセットされていない場合はよいが、セットされていた場合は分離オブジェクトをsaveしようとしたとき、制約違反が起きると思われたので、次のように実行してみた。

    
    //新しいStaffインスタンス
    Staff staff = new Staff();
    staff.setStaffname("test5");
    staff.setPassword("pass5");
    
    
    StaffInfo info = new StaffInfo();
    info.setAge(new Integer(1));
    info.setSex(new Integer(1));
    
    //Staff→StaffInfoとカスケードさせるため
    //StaffにStaffInfoをセットする
    staff.setStaffInfo(info);
    
    //StaffInfoにStaffをセットする
    info.setStaff(staff);
    
    
        
    //Staffを保存 カスケードを設定しているのでStaffInfoも保存される。
    generalDAO.saveStaff(staff);
    
    //Staffがセットされているので、制約違反になるはず
    generalDAO.saveStaffInfo(info);

結果、予想通り、分離オブジェクトになったStaffInfoの保存の段で制約違反となった。

主キーの双方向一対一関連とくらべて

複数のテーブルが関連を持つ場合、検索の主体となるものが外部キーを持つほうが効率がよくなる。

例えば、[勤務先]<--一対一-->[ユーザー]<--一対一-->[住所]なる関連があったとすると、「ユーザー」テーブルに外部キーがあったほうが効率がよくなるのである。

one-to-oneとmany-to-oneの組み合わせならば、そのようにすることは可能であるが、one-to-oneとone-to-oneでは構造的にそれが出来ない。[勤務先][住所]が外部キーとして[ユーザー]のキーを持つようにしか出来ないのである。