Hibernate EntityManagerで双方向1対1で遅延ロードする方法 まとめ
自分の知識不足から混乱してしまいましたが、何とか理解することができそうです。とりあえずまとめなど。
- Hibernate EntityManagerで1対1の関連に対してLAZYロードを行うには、その関連する永続化オブジェクトがnullかどうかがわからなければいけません。
- 外部キーを持っていて、その外部キーによって関連付けているオブジェクトに対しては、LAZYロードは可能です。
- 他のオブジェクトから外部キーによって参照されている側のオブジェクトは、相手がnullかどうか判断することが出来ないので、相手側のオブジェクトに対してLAZYロードを行うことが出来ません。
- ただし、相手が必ず存在することがわかっている場合、関連に対してnot null制約をつけることによって、LAZYロードを有効にすることが出来ます。
- 当然、登録時に関連するオブジェクトが既に永続化されているか、同時に登録する必要があります。
- 外部キー制約を持つ方の永続化クラスは、Hibernateが相手側を先に登録してくれるのでCASCADEが可能。逆の場合は、相手側の外部キーがnot nullの場合、相手側を先に登録することは不可能なので、CASCADEは利用できません。外部キーがnull okの場合は可能ですが、一旦永続化オブジェクトを登録した後、外部キーをupdateするので、一つのオブジェクトの登録に対してSQL文が2回発生してしまいます。
さて、外部キーを使った1対1の場合と、主キーで結合した場合のクラス記述例を書いてみようと思います。まずは外部キーの場合
@Entity public class Test1 implements Serializable { // Fields /** * */ private static final long serialVersionUID = 7556192090172970471L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Version private Timestamp version; private String name; @OneToOne(fetch = FetchType.LAZY, optional = false) @PrimaryKeyJoinColumn(referencedColumnName = "test1_id") private Test2 test2; (setter、getter略) } @Entity public class Test2 implements Serializable { // Fields /** * */ private static final long serialVersionUID = 79887318817618572L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Version private Timestamp version; @OneToOne(fetch = FetchType.LAZY) private Test1 test1; private String name; (setter、getter略) } public class TestServiceImpl implements TestService { public static void main(String[] args) { SingletonS2ContainerFactory.init(); S2Container container = SingletonS2ContainerFactory.getContainer(); TestService service = (TestService) container.getComponent(TestService.class); service.execute(); } private EntityManager em; public void setEm(EntityManager em) { this.em = em; } /* (非 Javadoc) * @see test.entitytest.TestService#execute() */ public void execute() { Test1 test1 = new Test1(); test1.setName("TEST1"); Test2 test2 = new Test2(); test2.setName("TEST2"); test2.setTest1(test1); test1.setTest2(test2); em.persist(test1); em.persist(test2); em.flush(); em.clear(); test1 = (Test1) em.find(Test1.class, test1.getId()); System.out.println("TEST1.ID:" + test1.getId()); System.out.println("TEST1.NAME:" + test1.getName()); System.out.println("TEST1.VERSION:" + test1.getVersion()); System.out.println(); test2 = test1.getTest2(); System.out.println("TEST2.ID:" + test2.getId()); System.out.println("TEST2.NAME:" + test2.getName()); System.out.println("TEST2.VERSION:" + test2.getVersion()); System.out.println(); em.clear(); test2 = (Test2) em.find(Test2.class, test2.getId()); System.out.println("TEST2.ID:" + test2.getId()); System.out.println("TEST2.NAME:" + test2.getName()); System.out.println("TEST2.VERSION:" + test2.getVersion()); System.out.println(); test1 = test2.getTest1(); System.out.println("TEST1.ID:" + test1.getId()); System.out.println("TEST1.NAME:" + test1.getName()); System.out.println("TEST1.VERSION:" + test1.getVersion()); System.out.println(); } }
Test2が外部キー「test1_id」を持ってます。参照される側のTest1ですが、@OneToOneにoptional = falseの定義をつけても、mappedByを設定した場合はLAZYロードは有効になりませんでした。@PrimaryKeyJoinColumnで相手の外部キーを定義する必要があるみたいですね。
CASCADEは無効にしました。これで実行してみると・・・
DEBUG 2006-02-02 08:38:49,411 [main] BEGIN test.entitytest.TestServiceImpl#execute() DEBUG 2006-02-02 08:38:49,427 [main] トランザクションを開始しました DEBUG 2006-02-02 08:38:49,567 [main] 論理的なコネクションを取得しました Hibernate: /* insert test.generate.entity.Test1 */ insert into Test1 (version, name, id) values (?, ?, null) DEBUG 2006-02-02 08:38:49,599 [main] 論理的なコネクションを閉じました DEBUG 2006-02-02 08:38:49,599 [main] 論理的なコネクションを取得しました Hibernate: call identity() DEBUG 2006-02-02 08:38:49,614 [main] 論理的なコネクションを閉じました DEBUG 2006-02-02 08:38:49,614 [main] 論理的なコネクションを取得しました Hibernate: /* insert test.generate.entity.Test2 */ insert into Test2 (version, test1_id, name, id) values (?, ?, ?, null) DEBUG 2006-02-02 08:38:49,630 [main] 論理的なコネクションを閉じました DEBUG 2006-02-02 08:38:49,630 [main] 論理的なコネクションを取得しました Hibernate: call identity() DEBUG 2006-02-02 08:38:49,630 [main] 論理的なコネクションを閉じました DEBUG 2006-02-02 08:38:49,661 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test1 */ select test1x0_.id as id3_0_, test1x0_.version as version3_0_, test1x0_.name as name3_0_ from Test1 test1x0_ where test1x0_.id=? DEBUG 2006-02-02 08:38:49,708 [main] 論理的なコネクションを閉じました TEST1.ID:25 TEST1.NAME:TEST1 TEST1.VERSION:2006-02-02 08:38:49.536 DEBUG 2006-02-02 08:38:50,036 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test2 */ select test2x0_.id as id1_0_, test2x0_.version as version1_0_, test2x0_.test1_id as test4_1_0_, test2x0_.name as name1_0_ from Test2 test2x0_ where test2x0_.id=? DEBUG 2006-02-02 08:38:50,036 [main] 論理的なコネクションを閉じました TEST2.ID:25 TEST2.NAME:TEST2 TEST2.VERSION:2006-02-02 07:57:06.439 DEBUG 2006-02-02 08:38:50,083 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test2 */ select test2x0_.id as id1_0_, test2x0_.version as version1_0_, test2x0_.test1_id as test4_1_0_, test2x0_.name as name1_0_ from Test2 test2x0_ where test2x0_.id=? DEBUG 2006-02-02 08:38:50,130 [main] 論理的なコネクションを閉じました TEST2.ID:25 TEST2.NAME:TEST2 TEST2.VERSION:2006-02-02 07:57:06.439 DEBUG 2006-02-02 08:38:50,130 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test1 */ select test1x0_.id as id3_0_, test1x0_.version as version3_0_, test1x0_.name as name3_0_ from Test1 test1x0_ where test1x0_.id=? DEBUG 2006-02-02 08:38:50,130 [main] 論理的なコネクションを閉じました TEST1.ID:24 TEST1.NAME:TEST1 TEST1.VERSION:2006-02-02 07:57:06.376 DEBUG 2006-02-02 08:38:50,130 [main] トランザクションをコミットしました DEBUG 2006-02-02 08:38:50,130 [main] END test.entitytest.TestServiceImpl#execute() : null
OKです。やはり外部キーで定義した方が何かとやりやすいですね。
次は、前回もやった主キーによる結合。Hibernate依存ですが、@GenericGeneratorを使ってみます。
@Entity public class Test3 implements Serializable { /** * */ private static final long serialVersionUID = 8017003162202366988L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Version private Timestamp version; private String name; @OneToOne(fetch = FetchType.LAZY, optional = false) @PrimaryKeyJoinColumn private Test4 test4; (setter、getter略) } @Entity public class Test4 implements Serializable { // Fields /** * */ private static final long serialVersionUID = 2389114284883053161L; @Id @GeneratedValue(generator = "test3") @GenericGenerator(name = "test3", strategy = "foreign", parameters = { @Parameter(name = "property", value = "test3") }) private Integer id; @Version private Timestamp version; @OneToOne(fetch = FetchType.LAZY, optional = false) @PrimaryKeyJoinColumn private Test3 test3; private String name; (setter、getter略) }
Hibernateでの設定を参考に書いてみました。実行クラスは省略。これで実行してみると・・・
DEBUG 2006-02-02 08:42:54,575 [main] BEGIN test.entitytest.Test2ServiceImpl#execute() DEBUG 2006-02-02 08:42:54,575 [main] トランザクションを開始しました DEBUG 2006-02-02 08:42:54,731 [main] 論理的なコネクションを取得しました Hibernate: /* insert test.generate.entity.Test3 */ insert into Test3 (version, name, id) values (?, ?, null) DEBUG 2006-02-02 08:42:54,763 [main] 論理的なコネクションを閉じました DEBUG 2006-02-02 08:42:54,763 [main] 論理的なコネクションを取得しました Hibernate: call identity() DEBUG 2006-02-02 08:42:54,763 [main] 論理的なコネクションを閉じました DEBUG 2006-02-02 08:42:54,778 [main] 論理的なコネクションを取得しました Hibernate: /* insert test.generate.entity.Test4 */ insert into Test4 (version, name, id) values (?, ?, ?) DEBUG 2006-02-02 08:42:54,778 [main] 論理的なコネクションを閉じました DEBUG 2006-02-02 08:42:54,794 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test3 */ select test3x0_.id as id5_0_, test3x0_.version as version5_0_, test3x0_.name as name5_0_ from Test3 test3x0_ where test3x0_.id=? DEBUG 2006-02-02 08:42:54,841 [main] 論理的なコネクションを閉じました TEST3.ID:22 TEST3.NAME:TEST3 TEST3.VERSION:2006-02-02 08:42:54.7 DEBUG 2006-02-02 08:42:55,107 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test4 */ select test4x0_.id as id0_0_, test4x0_.version as version0_0_, test4x0_.name as name0_0_ from Test4 test4x0_ where test4x0_.id=? DEBUG 2006-02-02 08:42:55,107 [main] 論理的なコネクションを閉じました test.generate.entity.Test4@16dc861 TEST4.ID:22 TEST4.NAME:TEST4 TEST4.VERSION:2006-02-02 08:42:54.763 DEBUG 2006-02-02 08:42:55,107 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test4 */ select test4x0_.id as id0_0_, test4x0_.version as version0_0_, test4x0_.name as name0_0_ from Test4 test4x0_ where test4x0_.id=? DEBUG 2006-02-02 08:42:55,122 [main] 論理的なコネクションを閉じました TEST4.ID:22 TEST4.NAME:TEST4 TEST4.VERSION:2006-02-02 08:42:54.763 DEBUG 2006-02-02 08:42:55,169 [main] 論理的なコネクションを取得しました Hibernate: /* load test.generate.entity.Test3 */ select test3x0_.id as id5_0_, test3x0_.version as version5_0_, test3x0_.name as name5_0_ from Test3 test3x0_ where test3x0_.id=? DEBUG 2006-02-02 08:42:55,169 [main] 論理的なコネクションを閉じました TEST3.ID:22 TEST3.NAME:TEST3 TEST3.VERSION:2006-02-02 08:42:54.7 DEBUG 2006-02-02 08:42:55,169 [main] トランザクションをコミットしました DEBUG 2006-02-02 08:42:55,169 [main] END test.entitytest.Test2ServiceImpl#execute() : null
Test4のtest3フィールドにセットされたオブジェクトの主キーが自動的にTest4の主キーとして登録されたのがわかります。これはJPA標準で欲しい機能ですね。何故採用されなかったんだろう?・・・
CASCADEに関しては、アプリケーション毎に色々戦略はあるかと思うのですが、外部キー制約を持つ方からしか基本的には利用できないという認識でいいのかな?
余談ですが、Struts in Action日本版を読み直していたところ、少しだけ説明が載ってました。「関連オブジェクトが常に存在するならば概念的には可能」って書いてありますね。どうやら見落としていたみたいです。今回は(も?)自分の調べ方が悪くて、「どうしてこういう動きをするのか?」ということについて、しっかり考えようとしなかったのが問題だったと思います。色々と反省することが多いです・・・