Hibernate EntityManagerで双方向1対1で遅延ロードする方法 その2



http://d.hatena.ne.jp/da-yoshi/20060131/1138664559#c
id:koichik:20060131#1138732249


詳しい説明ありがとうございます!
Hibernateのサイトで双方向1対1(の外部キーが無い方)がLAZYロードできない理由について書いてあったのですが、英語力の弱さのため(苦笑)しっかり理解出来てませんでした。なるほど・・・たしかに関連する1対1のオブジェクトのnullチェックをしないと、PROXYで全て代替するわけにはいかない筈ですね。
前回試した方法は、当然nullチェックを行っていないので、相手が存在しないときにもPROXYオブジェクトがセットされてしまうことになりそう・・・
まずはそれを試してみました。
前回の、単一主キーによる結合のパターンで、実行クラスを以下の内容に変更

		Test3 test3 = new Test3();
		test3.setName("TEST3");
		em.persist(test3);
		
//		Test4 test4 = new Test4();
//		test4.setId(test3.getId());
//		test4.setName("TEST4");
//		test4.setTest3(test3);
//		test3.setTest4(test4);
//		em.persist(test4);
		
		em.flush();
		em.clear();
		
		test3 = (Test3) em.find(Test3.class, test3.getId());
		System.out.println("TEST3.ID:" + test3.getId());
		System.out.println("TEST3.NAME:" + test3.getName());
		System.out.println("TEST3.VERSION:" + test3.getVersion());
		System.out.println();
		Test4 test4 = test3.getTest4();
		System.out.println(test4);
		System.out.println("TEST4.ID:" + test4.getId());
		System.out.println("TEST4.NAME:" + test4.getName());
		System.out.println("TEST4.VERSION:" + test4.getVersion());
		System.out.println();
		
		em.clear();
		
		test4 = (Test4) em.find(Test4.class, test4.getId());
		System.out.println("TEST4.ID:" + test4.getId());
		System.out.println("TEST4.NAME:" + test4.getName());
		System.out.println("TEST4.VERSION:" + test4.getVersion());
		System.out.println();
		test3 = test4.getTest3();
		System.out.println("TEST3.ID:" + test3.getId());
		System.out.println("TEST3.NAME:" + test3.getName());
		System.out.println("TEST3.VERSION:" + test3.getVersion());
		System.out.println();

Test4のオブジェクトを作成しないので、本来なら呼び出したときにnullがセットされ、test3.getTest4()でnullが返されるはずですが・・・
実行結果は

Exception in thread "main" org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [test.generate.entity.Test4#16]
	at org.hibernate.ObjectNotFoundException.throwIfNull(ObjectNotFoundException.java:27)
	at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:65)
	at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:98)
	at org.hibernate.proxy.CGLIBLazyInitializer.intercept(CGLIBLazyInitializer.java:158)
	at test.generate.entity.Test4$$EnhancerByCGLIB$$69a79d2e.toString(<generated>)
	at java.lang.String.valueOf(String.java:2577)
	at java.io.PrintStream.print(PrintStream.java:616)
	at java.io.PrintStream.println(PrintStream.java:753)
	at test.entitytest.Test2ServiceImpl.execute(Test2ServiceImpl.java:52)
	at test.entitytest.Test2ServiceImpl$$EnhancedByS2AOP$$1a3bc8c.execute$$invokeSuperMethod$$(Test2ServiceImpl$$EnhancedByS2AOP$$1a3bc8c.java)
	at test.entitytest.Test2ServiceImpl$$EnhancedByS2AOP$$1a3bc8c$$MethodInvocation$$execute0.proceed(MethodInvocationClassGenerator.java)
	at org.seasar.extension.tx.RequiredInterceptor.invoke(RequiredInterceptor.java:40)
	at test.entitytest.Test2ServiceImpl$$EnhancedByS2AOP$$1a3bc8c$$MethodInvocation$$execute0.proceed(MethodInvocationClassGenerator.java)
	at org.seasar.framework.aop.interceptors.TraceInterceptor.invoke(TraceInterceptor.java:50)
	at test.entitytest.Test2ServiceImpl$$EnhancedByS2AOP$$1a3bc8c$$MethodInvocation$$execute0.proceed(MethodInvocationClassGenerator.java)
	at test.entitytest.Test2ServiceImpl$$EnhancedByS2AOP$$1a3bc8c.execute(Test2ServiceImpl$$EnhancedByS2AOP$$1a3bc8c.java)
	at test.entitytest.Test2ServiceImpl.main(Test2ServiceImpl.java:17)

デバッグモードでチェックしたら、やはり、本来存在しない筈のTest4にPROXYオブジェクトがセットされ、そのオブジェクトのメソッドが呼び出されたときにLAZYロードされ、このエラーが発生しました。


・・・つまり、前回の方法は1対1で関連オブジェクトのnot null制約を外す為には有効だけど、当然nullチェックに関して永続化オブジェクトに対して「嘘」をつくことになる・・・ということか・・・
幸い、今回自分が検討しているテーブルは、業務的には確実に1対1になります(登録時に同時にINSERTされる)。永続化オブジェクトとしての本来の形を考えれば、双方向LAZYロードがやりたければ、それぞれの関連をnot nullにすることを受け入れる必要がある・・・ということですね。
JPAの方式で id:koichik:20060131#1138732249 で教えていただいた方法を実行してみようと思います。
まず、クラスの関連定義を、再度@OneToOneに変更

@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;

//	@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
//	@JoinColumn(name = "id", unique = true, insertable = false, updatable = false)
	@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
	private Integer id;

	@Version
	private Timestamp version;

//	@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
//	@JoinColumn(name = "id", unique = true, insertable = false, updatable = false)
	@OneToOne(fetch = FetchType.LAZY, optional = false)
	@PrimaryKeyJoinColumn
	private Test3 test3;

	private String name;

(setter、getter略)

}

実行クラスの書き方がちょっと難しかったです。まずは関連オブジェクトがnot nullになるので、当然Test3を登録するときに関連するTest4を作成してsetしておかなければいけません。しかしJPAには外部キーを利用するIDgeneratorが存在しないので、Test4は一旦IDを入れない状態で作成しなければいけません。この状態でTest3をpersistし(CASCADEを無効にしておかないとここでエラー)、その後Test3のIDをTest4にセットしてあげて、そしてTest4をpersist・・・というやり方になりそうです。

Test3 test3 = new Test3();
		test3.setName("TEST3");
//		em.persist(test3);
		
		Test4 test4 = new Test4();
		test4.setName("TEST4");
		test4.setTest3(test3);
		test3.setTest4(test4);
		em.persist(test3);
		test4.setId(test3.getId());
		em.persist(test4);

これで実行すると・・・

DEBUG 2006-02-01 08:28:49,656 [main] BEGIN test.entitytest.Test2ServiceImpl#execute()
DEBUG 2006-02-01 08:28:49,671 [main] トランザクションを開始しました
DEBUG 2006-02-01 08:28:49,953 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.Test3
        */ insert 
        into
            Test3
            (version, name, id) 
        values
            (?, ?, null)
DEBUG 2006-02-01 08:28:49,968 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-01 08:28:49,968 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-02-01 08:28:49,968 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-01 08:28:50,000 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.Test4
        */ insert 
        into
            Test4
            (version, name, id) 
        values
            (?, ?, ?)
DEBUG 2006-02-01 08:28:50,000 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-01 08:28:50,015 [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-01 08:28:50,140 [main] 論理的なコネクションを閉じました
TEST3.ID:19
TEST3.NAME:TEST3
TEST3.VERSION:2006-02-01 08:28:49.875

DEBUG 2006-02-01 08:28:50,421 [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-01 08:28:50,421 [main] 論理的なコネクションを閉じました
test.generate.entity.Test4@1124746
TEST4.ID:19
TEST4.NAME:TEST4
TEST4.VERSION:2006-02-01 08:28:49.984

DEBUG 2006-02-01 08:28:50,453 [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-01 08:28:50,453 [main] 論理的なコネクションを閉じました
TEST4.ID:19
TEST4.NAME:TEST4
TEST4.VERSION:2006-02-01 08:28:49.984

DEBUG 2006-02-01 08:28:50,484 [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-01 08:28:50,484 [main] 論理的なコネクションを閉じました
TEST3.ID:19
TEST3.NAME:TEST3
TEST3.VERSION:2006-02-01 08:28:49.875

DEBUG 2006-02-01 08:28:50,484 [main] トランザクションをコミットしました
DEBUG 2006-02-01 08:28:50,484 [main] END test.entitytest.Test2ServiceImpl#execute() : null

うまくいきました!
代理キーを使ってる場合、外部キーから自動的にセットできない関係でややこしくなってしまいますね・・・今回自分が利用しようとしているテーブルは複合自然キー使いまくりなので(汗)、幸か不幸か、単純にIDを両方のオブジェクトにセットするだけで済みます。また、CASCADEに関しては、自然キーを使っている関連については、外部キー制約を持つ方からCASCADEするようにすればうまく動くみたいです。ただ・・・普通制約がある方が「子」のような気がするので、基本的にCASCADEについては制限があると思った方がいいのかも・・・
(追記)Hibernateの@GenericGeneratorを使えば、Hibernate依存設定になってしまいますが、Hibernateのように外部キーを自動で主キー登録できるかも? 後で試してみたいと思います。
Hibernate EntityManagerのLAZYロードについては、今まで単純に考えてしまっていたのですが、なかなか複雑な問題を抱えていたんだなということが今回わかりました。最初に自分が試した方法(@ManyToOneで無理矢理設定する)というのは、関連が無いオブジェクトに嘘のPROXYオブジェクトが存在することになるのでわかりづらいし、存在チェックを例外ハンドリングで行わなければいけなくなるし・・・少なくとも優先的に選択する方法ではないような気がします。まずは、関連付けようとする定義が「必ず1対1」になるかどうかを調査し、その条件に当てはまる場合に、関連定義にnot null制約をつけてLAZYロードを有効にする・・・という順番で検討すべきなのかなと思いました。
・・・そういえば、テストするときにまずは@Proxyをつけずに試してみたのですが、このアノテーションをつけなくてもうまく動きました。Hibernate EntityManagerの方で、初期化時に自動的に設定してるのでしょうか?・・・
なにはともあれ、今回も非常に勉強になりました。ひがさん、koichiさん、ありがとうございました!