EJB 3.0(Public Draft)入門記 Java Persistence API Chapter2 その10

今回で2.1.8 Relationship Mapping Defaults を終わらせたいとおもいます。

2.1.8.5 Unidirectional Multi-Valued Relationships

単方向の複数リレーションシップについてです。
単方向の複数リレーションシップとは次のようなものです。

  • エンティティ A がエンティティ B のコレクションを参照する
  • エンティティ B はエンティティ A を参照しない

単方向のリレーションシップにはひとつの所有側があります。上記の場合、エンティティ A が所有側となります。
単方向の複数リレーションシップは単方向のOneToManyもしくは単方向のManyToManyのリレーションシップとして指定できるそうです。

2.1.8.5.1 Unidirectional OneToMany Relationships

まずは単方向のOneToManyから。エンティティEmployeeとAnnualReviewがあります。それぞれ上記のエンティティ A とエンティティ Bに相当します。

@Entity(access = AccessType.FIELD)
public class Employee implements Serializable {

  @Id
  private int id;

  private String name;

  @OneToMany
  private Collection annualReviews;

  // getter,setter省略
}
@Entity(access=AccessType.FIELD)
public class AnnualReview implements Serializable {

  @Id
  private int id;
  
  private String name;
  
  // getter,setter省略
}

エンティティ間の関係は次のとおりです。

  • エンティティEmployeeがエンティティAnnualReviewのコレクションを参照する
  • エンティティAnnualReviewはエンティティEmployeeを参照しない
  • エンティティEmployeeがリレーションシップの所有側

次のマッピングDefaultが適用されるそうです。

  • エンティティEmployeeはテーブルEMPLOYEEにマップされる
  • エンティティAnnualReviewはテーブルANNUALREVIEWにマップされる
  • EMPLOYEE_ANNUALREVIEW(所有側が先)という名前の関連テーブルが必要。この関連テーブルは次の二つの外部キーをもつ。
    • ひとつはテーブルEMPLOYEEを参照するもので、型はEMPLOYEEのプライマリキーと一緒。外部キーのカラム名は「EMPLOYEE_」。はEMPLOYEEテーブルのプライマリキーのカラムをあらわしている。つまりこの例では外部キーのカラム名は「EMPLOYEE_ID」となる。
    • もうひとつはテーブルANNUALREVIEWを参照するもので、型はANNUALREVIEWのプライマリキーと一緒。外部キーのカラム名は「ANNUALREVIEWS_」。はEMPLOYEEテーブルのプライマリキーのカラムをあらわしている。つまりこの例では外部キーのカラム名は「ANNUALREVIEWS_ID」となる。この外部キーはユニーク制約をもつ。

関連テーブルが必要なんですね。関連テーブルが必要なときってManyToManyのときだけかと思ってました。たしかに関連が単方向でOneの側からManyの側へとたどるには関連テーブルなしでは無理です。それにOneToManyであることを示すために関連テーブルにはユニーク制約が必要です。

上記のエンティティからhbm2ddlを介して作成されるDDLです。マッピングDefaultにしたがっているようです。

CREATE TABLE ANNUALREVIEW(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE EMPLOYEE(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE EMPLOYEE_ANNUALREVIEW(EMPLOYEE_ID INTEGER NOT NULL,ANNUALREVIEWS_ID INTEGER NOT NULL,
 CONSTRAINT SYS_CT_19 UNIQUE(ANNUALREVIEWS_ID),
 CONSTRAINT FK7FDEA4085CC2CD8F FOREIGN KEY(EMPLOYEE_ID) REFERENCES EMPLOYEE(ID),
 CONSTRAINT FK7FDEA4081DA70D0A FOREIGN KEY(ANNUALREVIEWS_ID) REFERENCES ANNUALREVIEW(ID))


実際に動かしてみます。データを作成します。

INSERT INTO ANNUALREVIEW VALUES(1,'給料')
INSERT INTO ANNUALREVIEW VALUES(2,'所属部署')
INSERT INTO EMPLOYEE VALUES(1,'ゴン')
INSERT INTO EMPLOYEE_ANNUALREVIEW VALUES(1,1)
INSERT INTO EMPLOYEE_ANNUALREVIEW VALUES(1,2)

次のプログラムでエンティティを取得してみます。

@Stateless
public class ClientBean implements Client {

  @PersistenceContext
  private EntityManager em;

  public void main() {
    // Employeeからたどる
    System.out.println("## Employeeからたどる ##");
    Employee employee = em.find(Employee.class, 1);
    System.out.println(employee.getName());
    for (AnnualReview a : employee.getAnnualReviews()) {
      System.out.println(" " + a.getName());
    }
  }

  public static void main(String[] args) throws Exception {
    EJB3StandaloneBootstrap.boot(null);
    EJB3StandaloneBootstrap.deployXmlResource("ejb3-deployment.xml");

    InitialContext ctx = new InitialContext();
    Client c = (Client) ctx.lookup(Client.class.getName());
    c.main();

    EJB3StandaloneBootstrap.shutdown();
  }
}

実行結果(SQLのログ出力含む)です。

## Employeeからたどる ##
Hibernate: select employee0_.id as id1_0_, employee0_.name as name1_0_ from Employee employee0_ where employee0_.id=?
ゴン
Hibernate: select annualrevi0_.Employee_id as Employee1_0_, annualrevi0_.annualReviews_id as annualRe2_0_ from Employee_AnnualReview annualrevi0_ where annualrevi0_.Employee_id=?
Hibernate: select annualrevi0_.id as id0_0_, annualrevi0_.name as name0_0_ from AnnualReview annualrevi0_ where annualrevi0_.id=?
 給料
Hibernate: select annualrevi0_.id as id0_0_, annualrevi0_.name as name0_0_ from AnnualReview annualrevi0_ where annualrevi0_.id=?
 所属部署

Lazy Loadingが行われています。EMPLOYEEテーブル、EMPLOYEE_ANNUALREVIEWテーブル、ANNUALREVIEWテーブルの順でJOINされることなく読まれています。


2.1.8.5.2 Unidirectional OneToMany Relationships

次に単方向のManyToManyです。エンティティEmployeeとPatentがあります。

@Entity(access = AccessType.FIELD)
public class Employee implements Serializable {

  @Id
  private int id;

  private String name;

  @ManyToMany
  private Collection patents;

  //getter,setter省略
}
@Entity(access=AccessType.FIELD)
public class Patent implements Serializable {

  @Id
  private int id;
  
  private String name;
  
  //getter,setter省略
}

エンティティ間の関係は次のとおりです。

  • エンティティEmployeeがエンティティPatentのコレクションを参照する
  • エンティティPatentはエンティティEmployeeを参照しない
  • エンティティEmployeeがリレーションシップの所有側

次のマッピングDefaultが適用されるそうです。

  • エンティティEmployeeはテーブルEMPLOYEEにマップされる
  • エンティティPatentはテーブルPATENTにマップされる
  • EMPLOYEE_PATENT(所有側が先)という名前の関連テーブルが必要。この関連テーブルは次の二つの外部キーをもつ。
    • ひとつはテーブルEMPLOYEEを参照するもので、型はEMPLOYEEのプライマリキーと一緒。外部キーのカラム名は「EMPLOYEE_」。はEMPLOYEEテーブルのプライマリキーのカラムをあらわしている。つまりこの例では外部キーのカラム名は「EMPLOYEE_ID」となる。
    • もうひとつはテーブルPATENTを参照するもので、型はPATENTのプライマリキーと一緒。外部キーのカラム名は「PATENTS_」。はEMPLOYEEテーブルのプライマリキーのカラムをあらわしている。つまりこの例では外部キーのカラム名は「PATENTS_ID」となる。

上記のエンティティからhbm2ddlを介して作成されるDDLです。マッピングDefaultにしたがっているようです。OneToManyのときと違ってユニーク制約ついてないです。

CREATE TABLE EMPLOYEE(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE EMPLOYEE_PATENT(EMPLOYEE_ID INTEGER NOT NULL,PATENTS_ID INTEGER NOT NULL,
 CONSTRAINT FKAC0E6C195CC2CD8F FOREIGN KEY(EMPLOYEE_ID) REFERENCES EMPLOYEE(ID))
CREATE TABLE PATENT(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
ALTER TABLE EMPLOYEE_PATENT ADD CONSTRAINT FKAC0E6C196DE44B0C FOREIGN KEY(PATENTS_ID) REFERENCES PATENT(ID)


実際に動かしてみます。
データを用意します。

INSERT INTO EMPLOYEE VALUES(1, 'ゴン')
INSERT INTO EMPLOYEE VALUES(2, 'うさはな')
INSERT INTO PATENT VALUES(1, '特許1')
INSERT INTO PATENT VALUES(2, '特許2')
INSERT INTO EMPLOYEE_PATENT VALUES(1, 1)
INSERT INTO EMPLOYEE_PATENT VALUES(1, 2)
INSERT INTO EMPLOYEE_PATENT VALUES(2, 1)

次のプログラムでエンティティを取得してみます。

@Stateless
public class ClientBean implements Client {

  @PersistenceContext
  private EntityManager em;

  public void main() {
    // Employeeからたどる
    System.out.println("## Employeeからたどる ##");
    Employee employee = em.find(Employee.class, 1);
    System.out.println(employee.getName());
    for (Patent p : employee.getPatents()) {
      System.out.println(" " + p.getName());
    }
  }

  public static void main(String[] args) throws Exception {
    EJB3StandaloneBootstrap.boot(null);
    EJB3StandaloneBootstrap.deployXmlResource("ejb3-deployment.xml");

    InitialContext ctx = new InitialContext();
    Client c = (Client) ctx.lookup(Client.class.getName());
    c.main();

    EJB3StandaloneBootstrap.shutdown();
  }
}

実行結果(SQLのログ出力含む)です。

## Employeeからたどる ##
Hibernate: select employee0_.id as id0_0_, employee0_.name as name0_0_ from Employee employee0_ where employee0_.id=?
ゴン
Hibernate: select patents0_.Employee_id as Employee1_0_, patents0_.patents_id as patents2_0_ from Employee_Patent patents0_ where patents0_.Employee_id=?
Hibernate: select patent0_.id as id1_0_, patent0_.name as name1_0_ from Patent patent0_ where patent0_.id=?
 特許1
Hibernate: select patent0_.id as id1_0_, patent0_.name as name1_0_ from Patent patent0_ where patent0_.id=?
 特許2

Lazy Loadingが行われています。EMPLOYEEテーブル、EMPLOYEE_PATENTテーブル、PATENTテーブルの順でJOINされることなく読まれています。
ところでemployee.getPatents()で取得できるCollectionの実際のクラスは何かとしらべてみたらorg.hibernate.collection.PersistentBagでした。Bagって何だっけ?

OneToManyとManyToManyを使った場合の違いは、関連テーブルから関連の非所有側へのテーブルを参照する外部キーにユニーク制約をつけるかどうかのようです。


ふー、やっと2.1.8が終わります。

リレーションシップのところは前からややこしという印象を持っていたのですが理由がわかりました。OneToManyが曲者なんです。OneToManyは双方向のリレーションシップでManyToOneと一緒につかわれるときと単方向のリレーションシップで単独で使われるときでテーブルの構造が全然ちがいます。こいつをおさえとけば比較的すっきりしそうです。
2.1.8ではテーブル名やカラム名のマッピングDefaultについて説明されてましたが、Default設定に従う場合には次のことが言えそうです。

  • テーブル名はEMPLOYEESなどの複数形にしない。テーブル名とエンティティ名がDefaultでマッピングされるからです。複数形はエンティティのコレクションの変数名にとっておきたいところです。
  • プライマリキーにはテーブル名を含めずIDとする。たとえばDEPARTMENTテーブルのプライマリキーがDEPARTMENT_IDでこのときEMPLOYEEテーブルの外部キーがDEPARTMENT_IDを参照する場合、Defaultのマッピングに従うとEMPLOYEEテーブルの外部キーのカラム名はDEPARTMENT_DEPARTMENT_IDといった冗長なものになってしまいます。