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

今回は継承についてです。平日は時間とれなくてあんまり入門記すすめられないので休日にまとめてすすめています。しかし、ドキュメントを単になぞるだけが入門記じゃないはず、ということでもう少しポイントを絞って進めたいなぁと思う今日このごろです。

2.1.9 Inheritance

すでにどこかで同じようなことが出てきたような気がしますが、エンティティの継承に関する特徴が挙げられてます。

  • エンティティは別のエンティティを継承することができる。このときエンティティのプライマリキーはスーパークラスとサブクラスで同じ型でなければいけない。
  • エンティティは抽象クラス、具象クラスどちらでもOK。
  • エンティティが非エンティティを継承することも非エンティティがエンティティを継承することもOK。

2.1.9.1 Abstract Entity Classes

抽象エンティティは具象エンティティとくらべて直接インスタンス化できないという点のみが異なるそうです。
例が載っています。ちょっとだけに変更してます。新しくアノテーションが出てきているのですが説明がないです。説明していないものを例で出すのやめてほしいなぁ。@Tableと@PrimaryKeyJoinColumnはだいたいわかるんですが、@InheritanceのdiscriminatorValueはなんでしょ。とりあえずほっておきます。この例には抽象エンティティEmployeeとこれを継承した具象エンティティが2つあります。

@Entity(access = AccessType.FIELD)
@Table(name = "EMP")
@Inheritance(strategy=InheritanceType.JOINED)
public abstract class Employee implements Serializable {

  @Id
  protected int id;

  protected String name;

  @ManyToOne
  protected Address address;
  
  // getter, setter省略
}
@Entity(access = AccessType.FIELD)
@Table(name = "FT_EMP")
@Inheritance(discriminatorValue = "FT")
@PrimaryKeyJoinColumn(name="FT_EMPID")
public class FullTimeEmployee extends Employee {
  
  private Integer salary;
  
  // getter, setter省略

}
@Entity(access=AccessType.FIELD)
@Table(name = "PT_EMP")
@Inheritance(discriminatorValue = "PT")
public class PartTimeEmployee extends Employee{

  private Float hourlyWage;
  
  // getter, setter省略

}
@Entity(access=AccessType.FIELD)
public class Address implements Serializable {
  @Id
  private int id;

  private String name;
  
  // getter, setter省略
}

上記のエンティティはhbm2ddl経由で次のようなDDLとなるようです。なるほど、感じがつかめた。差分だけ別テーブルでもってそのテーブルが親のテーブルを参照するんですね。

CREATE TABLE ADDRESS(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE EMP(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255),ADDRESS_ID INTEGER,
 CONSTRAINT FK10CA883B77ADC FOREIGN KEY(ADDRESS_ID) REFERENCES ADDRESS(ID))
CREATE TABLE FT_EMP(FT_EMPID INTEGER NOT NULL PRIMARY KEY,SALARY INTEGER,
 CONSTRAINT FK7C3F2DB72A308A75 FOREIGN KEY(FT_EMPID) REFERENCES EMP(ID))
CREATE TABLE PT_EMP(ID INTEGER NOT NULL PRIMARY KEY,HOURLYWAGE FLOAT,
 CONSTRAINT FK8D4FA3ED2646F65E FOREIGN KEY(ID) REFERENCES EMP(ID))


エンティティにアクセスしてみます。今回は実行されるSQLに着目してみます。
次のようにfindメソッドにスーパークラスを渡した場合(変数emはEntityManagerのインスタンスです)

em.find(Employee.class, 1);
select 
  employee0_.id as id1_1_, 
  employee0_.name as name1_1_, 
  employee0_.address_department_id as address3_1_1_, 
  employee0_1_.salary as salary2_1_, 
  employee0_2_.hourlyWage as hourlyWage3_1_, 
  case 
     when employee0_1_.FT_EMPID is not null then 1 
    when employee0_2_.id is not null then 2 
    when employee0_.id is not null then 0 
  end as clazz_1_, 
  address1_.department_id as department1_0_0_, 
  address1_.name as name0_0_ 
from 
  EMP employee0_ 
   left outer join FT_EMP employee0_1_ 
     on employee0_.id=employee0_1_.FT_EMPID 
   left outer join PT_EMP employee0_2_ 
     on employee0_.id=employee0_2_.id 
   left outer join Address address1_ 
     on employee0_.address_department_id=address1_.department_id 
where employee0_.id=?

おぉ、スーパークラスに対応するテーブルとサブクラスのエンティティにマップされたテーブルすべてが外部結合されるようです。case式がつかわれているのもポイントでしょうか。


次に上記と同じデータをfindメソッドにサブクラスを渡して取得する場合(変数emはEntityManagerのインスタンスです)

em.find(FullTimeEmployee.class, 1);
select 
  fulltimeem0_.FT_EMPID as id1_1_, 
  fulltimeem0_1_.name as name1_1_, 
  fulltimeem0_1_.address_id as address3_1_1_, 
  fulltimeem0_.salary as salary2_1_, address1_.id as id0_0_, 
  address1_.name as name0_0_ 
from 
  FT_EMP fulltimeem0_ 
  inner join EMP fulltimeem0_1_ 
    on fulltimeem0_.FT_EMPID=fulltimeem0_1_.id 
  left outer join Address address1_ 
    on fulltimeem0_1_.address_id=address1_.id 
where fulltimeem0_.FT_EMPID=?

内部結合が行われています。子供のテーブルが特定されるから最初のSQLのように外部結合が必要ないわけですね。この違いは興味深いです。


2.1.9.2 Non-Entity Classes in the Entity Inheritance Hierarchy

エンティティは非エンティティをスーパークラスとすることができるそうです。ただ非エンティティクラスは振る舞いだけをもちステートは永続性がないらしいです。そのため非エンティティはEntityManagerやQueryインタフェースに引数としてわたすことができず、マッピング情報をもつこともできないらしいです。
振る舞いだけならStrategy使ったほうがいいんじゃ?あんまり使わないかも。例は省略しちゃいます。


2.1.9.3 Embeddable Superclasses

エンティティは埋め込み可能なスーパークラスをもつことができるそうです。埋め込み可能なスーパークラスは永続的なエンティティのステートやマッピング情報を提供するけどもそれ自身はエンティティじゃないということです。埋め込み可能なスーパークラスの目的は複数のエンティティクラスに共通のステートやマッピング情報を定義することらしいです。
その他埋め込み可能なスーパークラスの特徴を箇条書き

  • EntityManagerやQueryインタフェースのメソッドに引数として渡せない
  • 具象クラスでも抽象クラスでもOK
  • EmbeddableSuperClassアノテーションで定義する
  • 対応するテーブルはもたない。マッピング情報は子エンティティに対応するテーブルにもつ。
  • 埋め込み可能なスーパークラスマッピング情報はサブクラスにAttributeOverrideアノテーションを指定することによりオーバーライド可能

サンプルです。ドキュメントのものを少し変更してます。AttributeOverrideがつかわれていたんですが、うまくいかなかったのではずしちゃいました。埋め込み可能なスーパークラスEmployeeとこれを継承する2つのクラスがあります。

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

  @Id
  private int id;

  private String name;

  @ManyToOne
  @JoinColumn(name="ADDR")
  private Address address;
  
  // getter, setter省略
  
}
@Entity(access = AccessType.FIELD)
@Table(name = "FT_EMP")
public class FullTimeEmployee extends Employee {
  
  private Integer salary;
  
  // getter, setter省略

}
@Entity(access=AccessType.FIELD)
@Table(name = "PT_EMP")
public class PartTimeEmployee extends Employee{

  private Float hourlyWage;

  // getter, setter省略
}
@Entity(access=AccessType.FIELD)
public class Address implements Serializable {
  @Id
  private int id;

  private String name;
  
  // getter, setter省略
}

生成されるDDLです。埋め込み可能なスーパークラスのフィールドに対応するカラムがFT_EMPテーブルとPT_EMPそれぞれに含まれています。

CREATE TABLE ADDRESS(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE FT_EMP(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255),SALARY INTEGER,ADDR INTEGER,
 CONSTRAINT FK7C3F2DB7D37C0EB0 FOREIGN KEY(ADDR) REFERENCES ADDRESS(ID))
CREATE TABLE PT_EMP(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255),HOURLYWAGE FLOAT,ADDR INTEGER,
 CONSTRAINT FK8D4FA3EDD37C0EB0 FOREIGN KEY(ADDR) REFERENCES ADDRESS(ID))


実行されるSQLを確認してみます。
findメソッドにサブクラスを渡して取得します(変数emはEntityManagerのインスタンスです)

em.find(FullTimeEmployee.class, 1);

そのとき実行されるSQLです。

select 
  fulltimeem0_.id as id1_1_, 
  fulltimeem0_.name as name1_1_, 
  fulltimeem0_.ADDR as ADDR1_1_, 
  fulltimeem0_.salary as salary1_1_, 
  address1_.id as id0_0_, 
  address1_.name as name0_0_ 
from 
  FT_EMP fulltimeem0_ 
  left outer join Address address1_ 
    on fulltimeem0_.ADDR=address1_.id 
where fulltimeem0_.id=?

埋め込み可能なスーパークラスを継承しているとかは関係なくSQLは通常のManyToOneのときと同じだと思います。
findメソッドにスーパークラスを渡した場合はExceptionがスローされました。埋め込み可能なスーパークラスは引数でわたしちゃいけないのでExceptionがおきるのは当然ですが。