Hibernate EntityManager Table per class hierarchy と Generics

前回のまとめの中で、1対1でLAZYロードを行うときのCASCADE設定について、「外部キー制約を持つ方からであれば可能」とだけ書いていたのですが、この内容についてkoichikさんが詳しく説明されてます。


id:koichik:20060202#1138899609


自分は単純に、関連定義にnot null制約がつくからオブジェクトを入れておく必要があるとだけしか考えてなかったのですが、よく考えれば、それぞれが双方向で関連定義していて、お互いがお互いを先に登録しようとしてるわけだから・・・ややこしいことになってしまいますね(汗)個人的には、「制約がある方からだけCASCADEできる」という点にも引っかかってますし、koichikさんがおっしゃられている通り、CASCADEは使わない方がいい気がしました。


さて、関連定義がひと段落したので、今は色々とプロトタイプ的なものを作ってます。慣れてくると非常に便利なツールであることを実感してます。
作ってみてちょっと便利だなと感じたのが、継承戦略の「Table per class hierarchy」をJavaGenericsと組み合わせるパターン。同じテーブルに種類の異なるデータが入っていて、それぞれを別のクラスで管理したいときに便利かも?
ちょっとサンプルを作ってみました。

CREATE TABLE TEST(
ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY,
TYPE VARCHAR,
NAME VARCHAR,
A VARCHAR,
B VARCHAR,
C VARCHAR,
VERSION TIMESTAMP
)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
public abstract class Test implements Serializable {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	private String name;
	
	@Version
	private Timestamp version;

(setter、getter略)

}

@Entity
@DiscriminatorValue("A")
public class TestA extends Test {

	/**
	 * 
	 */
	private static final long serialVersionUID = 4370683195807722348L;
	
	private String a;

	public String getA() {
		return a;
	}

	public void setA(String a) {
		this.a = a;
	}

}

@Entity
@DiscriminatorValue("B")
public class TestB extends Test {

	/**
	 * 
	 */
	private static final long serialVersionUID = -9197622401333982203L;
	
	private String b;

	public String getB() {
		return b;
	}

	public void setB(String b) {
		this.b = b;
	}

}

@Entity
@DiscriminatorValue("C")
public class TestC extends Test {

	/**
	 * 
	 */
	private static final long serialVersionUID = -7581619183851933322L;
	
	private String c;

	public String getC() {
		return c;
	}

	public void setC(String c) {
		this.c = c;
	}

}

テストテーブル定義にひねりが無くてすみません(汗)
TESTテーブルのtypeカラムを使って、Table per clas hierarchy戦略をとります。TestA、TestB、TestCというクラスはそれぞれTestクラスを継承し、それぞれ独自のフィールド a、b、c を持ちます。
これらのクラスの登録、検索処理は

	public void execute() {
		
		TestA a = new TestA();
		a.setName("A1");
		a.setA("A1");
		em.persist(a);
		
		a = new TestA();
		a.setName("A2");
		a.setA("A2");
		em.persist(a);

		TestB b = new TestB();
		b.setName("B1");
		b.setB("B1");		
		em.persist(b);
		
		b = new TestB();
		b.setName("B2");
		b.setB("B2");
		em.persist(b);
		
		TestC c = new TestC();
		c.setName("C1");
		c.setC("C1");
		
		em.persist(c);
		
		c = new TestC();
		c.setName("C2");
		c.setC("C2");
		em.persist(c);
		
	}
	
	public <T extends Test> List<T> getList(Class<T> clazz) {
		
		return (List<T>) em.createQuery(
			"SELECT t FROM " + clazz.getSimpleName() + " t")
			.getResultList();
	}

検索メソッドにGenericsを使ってみました。EJB-QLの機能を利用して、Testクラスを継承したクラスの型定義を、戻り値のListの型に反映させます。
これの実行メソッドは

	public static void main(String[] args) {
		SingletonS2ContainerFactory.init();
		S2Container container = SingletonS2ContainerFactory.getContainer();
		SingleTableTestService service = (SingleTableTestService) container.getComponent(SingleTableTestService.class);
		service.execute();
		
		List<Test> list = service.getList(Test.class);
		for (Test t : list) {
			System.out.println(t.getId());
			System.out.println(t.getName());
			System.out.println(t.getVersion());
		}
		System.out.println();

		List<TestA> listA = service.getList(TestA.class);
		for (TestA t : listA) {
			System.out.println(t.getId());
			System.out.println(t.getName());
			System.out.println(t.getA());
			System.out.println(t.getVersion());
		}
		System.out.println();

		List<TestB> listB = service.getList(TestB.class);
		for (TestB t : listB) {
			System.out.println(t.getId());
			System.out.println(t.getName());
			System.out.println(t.getB());
			System.out.println(t.getVersion());
		}
		System.out.println();

		List<TestC> listC = service.getList(TestC.class);
		for (TestC t : listC) {
			System.out.println(t.getId());
			System.out.println(t.getName());
			System.out.println(t.getC());
			System.out.println(t.getVersion());
		}
		System.out.println();
	}

この実行結果は

DEBUG 2006-02-06 08:19:21,562 [main] BEGIN test.entitytest.SingleTableTestServiceImpl#execute()
DEBUG 2006-02-06 08:19:21,578 [main] トランザクションを開始しました
DEBUG 2006-02-06 08:19:21,859 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.TestA
        */ insert 
        into
            Test
            (name, version, a, type, id) 
        values
            (?, ?, ?, 'A', null)
DEBUG 2006-02-06 08:19:21,890 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,890 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-02-06 08:19:21,890 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,906 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.TestA
        */ insert 
        into
            Test
            (name, version, a, type, id) 
        values
            (?, ?, ?, 'A', null)
DEBUG 2006-02-06 08:19:21,906 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,906 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-02-06 08:19:21,906 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,906 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.TestB
        */ insert 
        into
            Test
            (name, version, b, type, id) 
        values
            (?, ?, ?, 'B', null)
DEBUG 2006-02-06 08:19:21,921 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,921 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-02-06 08:19:21,921 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,921 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.TestB
        */ insert 
        into
            Test
            (name, version, b, type, id) 
        values
            (?, ?, ?, 'B', null)
DEBUG 2006-02-06 08:19:21,921 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,921 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-02-06 08:19:21,921 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,937 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.TestC
        */ insert 
        into
            Test
            (name, version, c, type, id) 
        values
            (?, ?, ?, 'C', null)
DEBUG 2006-02-06 08:19:21,937 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,937 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-02-06 08:19:21,937 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,937 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.TestC
        */ insert 
        into
            Test
            (name, version, c, type, id) 
        values
            (?, ?, ?, 'C', null)
DEBUG 2006-02-06 08:19:21,953 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,953 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-02-06 08:19:21,953 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:21,968 [main] トランザクションをコミットしました
DEBUG 2006-02-06 08:19:21,968 [main] END test.entitytest.SingleTableTestServiceImpl#execute() : null
DEBUG 2006-02-06 08:19:21,968 [main] BEGIN test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.Test)
DEBUG 2006-02-06 08:19:21,968 [main] トランザクションを開始しました
DEBUG 2006-02-06 08:19:22,390 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* SELECT
        t 
    FROM
        Test t */ select
            test0_.id as id0_,
            test0_.name as name0_,
            test0_.version as version0_,
            test0_.c as c0_,
            test0_.a as a0_,
            test0_.b as b0_,
            test0_.type as type0_ 
        from
            Test test0_
DEBUG 2006-02-06 08:19:22,437 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:22,640 [main] トランザクションをコミットしました
DEBUG 2006-02-06 08:19:22,640 [main] END test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.Test) : [test.generate.entity.TestA@dec8b3, test.generate.entity.TestA@4a7df6, test.generate.entity.TestB@93df2c, test.generate.entity.TestB@20f443, test.generate.entity.TestC@18488ef, test.generate.entity.TestC@3a1834]
6
A1
2006-02-06 08:19:21.828
7
A2
2006-02-06 08:19:21.906
8
B1
2006-02-06 08:19:21.906
9
B2
2006-02-06 08:19:21.921
10
C1
2006-02-06 08:19:21.921
11
C2
2006-02-06 08:19:21.937

DEBUG 2006-02-06 08:19:22,640 [main] BEGIN test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.TestA)
DEBUG 2006-02-06 08:19:22,640 [main] トランザクションを開始しました
DEBUG 2006-02-06 08:19:22,656 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* SELECT
        t 
    FROM
        TestA t */ select
            testa0_.id as id0_,
            testa0_.name as name0_,
            testa0_.version as version0_,
            testa0_.a as a0_ 
        from
            Test testa0_ 
        where
            testa0_.type='A'
DEBUG 2006-02-06 08:19:22,656 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:22,656 [main] トランザクションをコミットしました
DEBUG 2006-02-06 08:19:22,656 [main] END test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.TestA) : [test.generate.entity.TestA@1754699, test.generate.entity.TestA@6e1dec]
6
A1
A1
2006-02-06 08:19:21.828
7
A2
A2
2006-02-06 08:19:21.906

DEBUG 2006-02-06 08:19:22,671 [main] BEGIN test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.TestB)
DEBUG 2006-02-06 08:19:22,671 [main] トランザクションを開始しました
DEBUG 2006-02-06 08:19:22,671 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* SELECT
        t 
    FROM
        TestB t */ select
            testb0_.id as id0_,
            testb0_.name as name0_,
            testb0_.version as version0_,
            testb0_.b as b0_ 
        from
            Test testb0_ 
        where
            testb0_.type='B'
DEBUG 2006-02-06 08:19:22,671 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:22,687 [main] トランザクションをコミットしました
DEBUG 2006-02-06 08:19:22,687 [main] END test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.TestB) : [test.generate.entity.TestB@18e4327, test.generate.entity.TestB@dada24]
8
B1
B1
2006-02-06 08:19:21.906
9
B2
B2
2006-02-06 08:19:21.921

DEBUG 2006-02-06 08:19:22,687 [main] BEGIN test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.TestC)
DEBUG 2006-02-06 08:19:22,687 [main] トランザクションを開始しました
DEBUG 2006-02-06 08:19:22,703 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* SELECT
        t 
    FROM
        TestC t */ select
            testc0_.id as id0_,
            testc0_.name as name0_,
            testc0_.version as version0_,
            testc0_.c as c0_ 
        from
            Test testc0_ 
        where
            testc0_.type='C'
DEBUG 2006-02-06 08:19:22,703 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-06 08:19:22,703 [main] トランザクションをコミットしました
DEBUG 2006-02-06 08:19:22,703 [main] END test.entitytest.SingleTableTestServiceImpl#getList(class test.generate.entity.TestC) : [test.generate.entity.TestC@221e9e, test.generate.entity.TestC@83e1e]
10
C1
C1
2006-02-06 08:19:21.921
11
C2
C2
2006-02-06 08:19:21.937

うまく動きました。継承戦略を具体的にどう利用したらいいのか色々考えてたのですが、このパターンを使えば、テーブル内の様々なデータを個別のクラスにマッピングして利用できそうですね。
他の継承戦略についても考えてみたのですが・・・まず「Table per concrete class」戦略は、Hibernate実装の場合は@MappedSuperClassでも同じことが出来るし、あまり必要性を感じません。@MappedSuperClassは他の継承戦略とも組み合わせて使えるので、こちらの方が便利だと思います。
また、「Table per subclass」戦略ですが、これも「Table per class hierarchy」で定義したサブクラスに対して1対1の関連定義を行えば、似たようなことが出来る気がします。JOINをサブクラス自体に定義してしまうと、その部分に対してLAZYロードが行えなくなるので、柔軟性に欠ける気がしますし・・・
まだまだ色々試してみないといけないのですが、現時点では「Table per class hierarchy」が最も使い易く、他の継承戦略の利点もうまく補えるパターンではないかと思ってます。
・・・あと余談ですが、今回Tomcat上で色々試しているのですが、その中で分離永続化オブジェクトをHttpSessionに保存するパターンを使ってみました(つまりDTOを使わないパターン)。これをやってる場合、HttpSessionが保持されている状態でTomcatを再起動しようとすると、TomcatがSessionを保存しようとして、例のLAZYロードエラーが多発します。再起動するときにユーザのSessionデータが保持されない前提なら大丈夫なんでしょうが・・・気持ち良いものではありませんね。分離オブジェクトの利用をHibernateEJB3も薦めているわけですが、その制限についてもしっかり記述して欲しいというか・・・こういうのは使ってみないとわからないですし・・・うーん、どうしよう・・・