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

前回は継承マッピング戦略、Inheritance Mapping Strategeiesでした。今日は2番目の戦略であるTable per Class Strategyです。


2.1.10.2 Table per Class Strategy

この戦略ではそれぞれのクラスが別々のテーブルにマッピングされます。クラスのすべてのプロパティ(継承したプロパティを含む)が対応するテーブルのカラムにマップされるそうです。言葉で理解するよりもコードとDDLをみたほうがわかります。
昨日の例に使ったコードをtable per class strategyを使うように変更してみました。ただし、今回はEntityManagerを使ってデータを作成するつもりなのでIdアノテーションにgenerate要素を指定してます。

@Entity(access=AccessType.FIELD)
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Employee implements Serializable {

  @Id(generate=GeneratorType.TABLE)
  private int id;

  private String name;

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

  private Integer salary;

  //getter, setter省略  
}

@Entity(access = AccessType.FIELD)
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です。

CREATE TABLE ADDRESS(
  ID INTEGER NOT NULL PRIMARY KEY,
  NAME VARCHAR(255))

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

CREATE TABLE FULLTIMEEMPLOYEE(
  ID INTEGER NOT NULL PRIMARY KEY,
  NAME VARCHAR(255),
  ADDRESS_ID INTEGER,
  SALARY INTEGER,
  CONSTRAINT FK4AFD4ACE233D5405D5BDCEA FOREIGN KEY(ADDRESS_ID) REFERENCES ADDRESS(ID))

CREATE TABLE PARTTIMEEMPLOYEE(
  ID INTEGER NOT NULL PRIMARY KEY,
  NAME VARCHAR(255),
  ADDRESS_ID INTEGER,
  HOURLYWAGE FLOAT,
  CONSTRAINT FK4AFD4ACE233D54059735284E FOREIGN KEY(ADDRESS_ID) REFERENCES ADDRESS(ID))
  
CREATE TABLE HIBERNATE_SEQUENCES(
  SEQUENCE_NAME VARCHAR(255),
  SEQUENCE_NEXT_HI_VALUE INTEGER)  

ポイントは

  • Inheritanceアノテーションの要素strategyに「InheritanceType.TABLE_PER_CLASS」と指定している
  • エンティティEmployeeをFullTimeEmployeeとPartTimeEmployeeが継承している
  • クラスに対応してEMPLOYEEとFULLTIMEEMPLOYEEとPARTTIMEEMPLOYEEのテーブルがある。
  • EMPLOYEEテーブルと同じカラムがFULLTIMEEMPLOYEEとPARTTIMEEMPLOYEEテーブルにもある。
  • HIBERNATE_SEQUENCESというテーブルがある。Employeeクラスに@Id(generate=GeneratorType.TABLE)と指定したからだと思われます。うーん、こんなものができるとは。採番テーブルらしい。

table per class strategyには次の欠点があるそうです。

  • ポリモーフィックな関連のサポートが貧弱(poor)
  • クラス階層をまたがる問い合わせにはSQLのUNIONクエリ(もしくはサブクラスごとの別々のクエリ)の発行が通常必要となる

ポリモーフィックな関連のサポートが貧弱というのはどういうことでしょう。ほかの戦略ではできることができない?できるけどパフォーマンス的に問題あり?
Hibernate in Actionにはテーブルが別々になってしまうため外部制約で参照しにくいみたいなことが書いてあります。


では動かしてみます。
いつもならSQLでデータを用意するところですが今回はどのようにデータを用意しておけばいいのかわからなかったのでEntityManagerを使ってデータを作ることにしました。いつもは自分でIDをINSERTしているのですが、今回はIdアノテーションに「GeneratorType.TABLE」と指定しておきました。「GeneratorType.AUTO」や「GeneratorType.IDENTITY」でないのはtable per class strategyではそれらは使えなかったからです。使ってみたらExceptionをくらいました。EJB 3.0の仕様というよりHibernateの実装?
「GeneratorType.TABLE」とはどういうものかというと、よくわかってないのですが、採番テーブルから値を取得するということだと思います。今回は自動的に作成された採番テーブルを使いますが、任意のものも使用できるのかもしれません。

データを作るために次のクラスを実行してみます。

@Stateless
public class CreateDataBean implements CreateData {

  @PersistenceContext
  private EntityManager em;

  public void main() {
    
    Address ad = new Address();
    ad.setName("京都");
    
    FullTimeEmployee ft = new FullTimeEmployee();
    ft.setName("ゴン");
    ft.setSalary(2000000);
    ft.setAddress(ad);

    PartTimeEmployee pt = new PartTimeEmployee();
    pt.setName("うさはな");
    pt.setAddress(ad);
    pt.setHourlyWage(900F);

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

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

    EJB3StandaloneBootstrap.shutdown();
  }
}

データは次のようになりました。
EMPLOYEE

 ID NAME ADDRESS_ID 
 -- ---- ---------- 

FULLTIMEEMPLOYEE

 ID NAME ADDRESS_ID SALARY  
 -- ---- ---------- ------- 
 1  ゴン   1          2000000 

PARTTIMEEMPLOYEE

 ID NAME ADDRESS_ID HOURLYWAGE 
 -- ---- ---------- ---------- 
 2  うさはな 1          900.0     

ADDRESS

 ID NAME 
 -- ---- 
 1  京都   

HIBERNATE_SEQUENCES

 SEQUENCE_NAME SEQUENCE_NEXT_HI_VALUE 
 ------------- ---------------------- 
 Employee      1     

なるほどFULLTIMEEMPLOYEEとPARTTIMEEMPLOYEEは違うIDになるんですね。EMPLOYEEにも同じようなデータが入るのかもと思ったのですがそうではないみたいです。HIBERNATE_SEQUENCESのSEQUENCE_NAMEにはEmployeeというクラス名とおぼしき値がはいってます。


上記のデータを取得するプログラム(前回と同じClientBean)を実行してみます。そのときの結果はこうなります。

## 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 address3_1_, 
  employees0_.id as id1_, 
  employees0_.id as id1_0_, 
  employees0_.name as name1_0_, 
  employees0_.address_id as address3_1_0_, 
  employees0_.salary as salary2_0_, 
  employees0_.hourlyWage as hourlyWage3_0_, 
  employees0_.clazz_ as clazz_0_ 
from 
  ( select 
      null as hourlyWage, 
      address_id, 
      name, 
      id, 
      null as salary, 
      0 as clazz_ 
    from 
      Employee 
    union 
    select 
      null as hourlyWage, 
      address_id, name, 
      id, salary, 
      1 as clazz_ 
  from
      FullTimeEmployee 
    union 
    select 
      hourlyWage, 
      address_id, 
      name, id, 
      null as salary, 
      2 as clazz_ 
    from 
      PartTimeEmployee 
  ) employees0_ 
where employees0_.address_id=?

Address取得のSQLが実行された後にすべてのEmployeeをとってくるSQLが実行されます。
それにしてもUNIONを使ったSQLってこうことですか。継承クラスが多くてもデータが多くても大変そう。ところで、SQLだけを抜き出したわけではないのですが出力はここまでです。本当はログ出力のあとにクラス名などが表示されるようにしてあるのですが出力されていない。実はEmployeeを取得するSQLの結果がHSQLDBから返ってこないのです。HSQLDBが耐えられないのか?


サブクラスのエンティティを直接EntitiyManagerに指定した場合のSQLを確かめて見ます。

em.find(FullTimeEmployee.class, 1);

SQLはこうなります。

select 
  fulltimeem0_.id as id1_1_, 
  fulltimeem0_.name as name1_1_, 
  fulltimeem0_.address_id as address3_1_1_, 
  fulltimeem0_.salary as salary2_1_, 
  address1_.id as id0_0_, 
  address1_.name as name0_0_ 
from 
  FullTimeEmployee fulltimeem0_ 
  left outer join Address address1_ 
    on fulltimeem0_.address_id=address1_.id 
where fulltimeem0_.id=?

こっちはちゃんと返ってきました。


今日はここまで。結構コピペ疲れました。
2.1.10.2 Table per Class Strategy終了です。

次回はJoined Subclass Strategyです。でも、これ2.1.9ですでに動かしたような。