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日本版を読み直していたところ、少しだけ説明が載ってました。「関連オブジェクトが常に存在するならば概念的には可能」って書いてありますね。どうやら見落としていたみたいです。今回は(も?)自分の調べ方が悪くて、「どうしてこういう動きをするのか?」ということについて、しっかり考えようとしなかったのが問題だったと思います。色々と反省することが多いです・・・