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


2.1.10 Inheritance Mapping Strategeies

今回は継承に関するマッピング戦略です。
クラスやクラス階層をRDBスキーママッピングするには3つの戦略があります。3番目は日本語にするの難しい。

  • single table per class hierarchy strategy ― ひとつのクラス階層にひとつのテーブル戦略
  • table per class strategy ― ひとつのクラスにつきひとつのテーブル戦略
  • joined subclass stragegy ― スーパークラスとサブクラスでテーブルを別にしクラスのインスタンス化時にはJoinをつかう戦略

EJB 3.0の実装がサーポートする必要があるのはsingle table per class hierarchyだけであとの2つは今回のリリースではオプショナルだそうです。次回のリリースでは必須になるかもということです。

2.1.10.1 Single Table per Class Hierarchy Strategy

では、まず EJB 3.0 に必須のsingle table per class hierarchy から。
この戦略ではクラス階層がひとつのテーブルにマップされ、テーブルは"discriminator column"として機能するカラムをもつそうです。discriminator columnとは、テーブルの各行から生成されるインスタンスが属するサブクラスを識別するカラムだそうです。
例えばEmployeeというスーパークラスがあり、このサブクラスとしてFullTimeEmployeeとPartTimeEmployeeがあるとします。single table per class hierarchyではこれらのクラス(たぶんスーパークラスも含む)はひとつのテーブルにマップされ、各レコードがインスタンスをあらわします。どのレコードがどのクラスかを識別するのがdiscriminator columnということだと思います。
discriminator columnの説明はkoichikさんのHibernate入門記が参考になります。http://d.hatena.ne.jp/koichik/20040726#1090862414
ところであんまり意識してませんでしたがPersistence APIの理解にHibernateを参考にするのは有効そうです。あたりまえか..。

single table per class hierarchyの戦略はエンティティ間のポリモーフィックな関連やクラス階層にまたがるクエリーをサポートするらしいです。ポリモーフィックな関連というのがよくわからなかったのですが、さっそくHibernateもちょっとは参考にしてみようということでほとんど読んだことのないHibernate in Actionを見てみました。僕の理解するところでは、ポリモーフィックな関連とはあるエンティティの関連先のエンティティに複数のサブクラスがある場合に実行時に具象クラスを意識することなくサブクラスのインスタンスを参照できることを言うみたいです。

single table per class hierarchy戦略の欠点はサブクラス固有のステートに対応するカラムにnullが入るのを認めなければいけないことだそうです。クラスAで使用されるカラムがクラスBにはマップされない場合に、クラスBをあらわすレコードではそのカラムにはnullが入るということですね。これは仕方がない。


では例です。ポリモーフィックな関連を使うためにManyToOne/OneToManyなリレーションシップも使ってみます。Employeeに継承関係があります。EmployeeはAddressから参照されてます。@Inheritanceで戦略のタイプとdiscriminator columnに入れる値を指定するようです。@DiscriminatorColumnはその名のとおりdiscriminator columnを指定します。指定されていない場合は「TYPE」がDefaultになるらしいです。今回は「EMP_TYPE」と指定してみました。

@Entity(access=AccessType.FIELD)
@Inheritance(strategy=InheritanceType.SINGLE_TABLE, discriminatorValue="EMP")
@DiscriminatorColumn(name="EMP_TYPE")
public class Employee implements Serializable {

  @Id
  private int id;

  private String name;

  @ManyToOne
  private Address address;
  
  // getter, setter省略
}
@Entity(access=AccessType.FIELD)
@Inheritance(discriminatorValue = "FT")
public class FullTimeEmployee extends Employee {

  private Integer salary;
  
  // getter, setter省略
}
@Entity(access = AccessType.FIELD)
@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;

  @OneToMany(mappedBy="address")
  private Collection employees = new HashSet();   
  
  // getter, setter省略
}

hbm2ddlが吐き出したDDLです。DDLをみればsingle table per class hierarchyが何を言っているか一目瞭然ですね。Employee系のエンティティは3つなのにひとつのテーブルにまとめられています。EMP_TYPEというカラムがポイントですね。

CREATE TABLE ADDRESS(ID INTEGER NOT NULL PRIMARY KEY,NAME VARCHAR(255))
CREATE TABLE EMPLOYEE(
  EMP_TYPE VARCHAR(10) NOT NULL,
  ID INTEGER NOT NULL PRIMARY KEY,
  NAME VARCHAR(255),
  SALARY INTEGER,
  HOURLYWAGE FLOAT,
  ADDRESS_ID INTEGER,
  CONSTRAINT FK4AFD4ACE233D5405 FOREIGN KEY(ADDRESS_ID) REFERENCES ADDRESS(ID))


では動かしてみます。
データ作ります。EMP_TYPEカラムのところは'FT'とか'PT'とか入れてやればいいんだと思います、たぶん。

INSERT INTO ADDRESS VALUES(1, '京都');
INSERT INTO EMPLOYEE VALUES('FT', 1, 'ゴン', 200000, null, 1);
INSERT INTO EMPLOYEE VALUES('PT', 2, 'うさはな', null, 850, 1);

実行します。Addressからたどってみます。

@Stateless
public class ClientBean implements Client {

  @PersistenceContext
  private EntityManager em;

  public void main() {
    System.out.println("## Addressからたどる ##");
    Address address = em.find(Address.class, 1);
    for(Employee e: address.getEmployees()) {
      System.out.print(e.getClass().getName() + " : ");
      System.out.println(e.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());
    //Client c = new ClientBean();
    c.main();

    EJB3StandaloneBootstrap.shutdown();
  }
}

実行結果(整形してます)。

## Addressからたどる ##
Hibernate: 
select 
  address0_.id as id0_0_, 
  address0_.name as name0_0_ 
from 
  Address address0_ 
where address0_.id=?

Hibernate: 
select 
  employees0_.address_id as address6_1_, 
  employees0_.id as id1_, 
  employees0_.id as id1_0_, 
  employees0_.name as name1_0_, 
  employees0_.address_id as address6_1_0_, 
  employees0_.salary as salary1_0_, 
  employees0_.hourlyWage as hourlyWage1_0_, 
  employees0_.EMP_TYPE as EMP1_1_0_ 
from 
  Employee employees0_ 
where 
  employees0_.address_id=?

study.ejb.entity.FullTimeEmployee : ゴン
study.ejb.entity.PartTimeEmployee : うさはな

ポリモーフィックな関連を使ったつもりです。FullTimeEmployeeとPartTimeEmployeeのインスタンスが取得できています。EMP_TYPEでレコードが識別されてそれぞれのインスタンスが生成されたと思われます。


ちょっと気になったのでサブエンティティ(そんな呼び方しないかも)を指定して取得してみました。こんなかんじ。

em.find(FullTimeEmployee.class, 1);

そのときのSQL

select 
  fulltimeem0_.id as id1_1_, 
  fulltimeem0_.name as name1_1_, 
  fulltimeem0_.address_id as address6_1_1_, 
  fulltimeem0_.salary as salary1_1_, address1_.id as id0_0_, 
  address1_.name as name0_0_ 
from Employee fulltimeem0_ 
  left outer join Address address1_ 
    on fulltimeem0_.address_id=address1_.id 
where 
      fulltimeem0_.id=? 
  and fulltimeem0_.EMP_TYPE='FT'

おぉ、このときは必要最低限のSQLになるんですね。賢いなぁ。


2.10.1は1回で終わる分量かなと思っていたのですが長くなってしまいました。
table per class strategy は次回にします。