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」をJavaのGenericsと組み合わせるパターン。同じテーブルに種類の異なるデータが入っていて、それぞれを別のクラスで管理したいときに便利かも?
ちょっとサンプルを作ってみました。
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データが保持されない前提なら大丈夫なんでしょうが・・・気持ち良いものではありませんね。分離オブジェクトの利用をHibernateもEJB3も薦めているわけですが、その制限についてもしっかり記述して欲しいというか・・・こういうのは使ってみないとわからないですし・・・うーん、どうしよう・・・