Hiernate EntityManagerで双方向1対1で遅延ロードする方法

一度Hibernate本体で試してみたおかげで、どうやらHibernate本体に絡む問題らしいことがわかってきました。というわけで、Hibernate本体に焦点を当ててググってみたところ、以下のページを発見しました。


Some explanations on lazy loading(Hibernate公式サイト)


どうやら標準の方法では、one-to-oneは単一方向からのLAZYロードしか出来ないみたいですね。双方向はもう諦めるしかないのかな・・・と思っていたところ、このページの最後の方に解決策らしき記述を発見!!
どうやら、property-refをつける側のone-to-oneの記述を、unique = true, insert = false, update = false で定義したmany-to-oneで置き換えると上手くいくらしいです。なるほど・・・Hibernateの双方向の関連は、双方向であることを記述しない限りは独立した定義になるから、それを逆に利用するってことか・・・
insert と update をfalseにする方法は、主キーを使った多対1の方法でも出てきましたね。なんだか出来そうな予感・・・
というわけで、昨日のサンプルで早速試してみました。あまり沢山やってもアレなので、とりあえず一番複雑な、複合主キーの例で動かしてみます。
マッピングファイルを以下の内容に変更

<hibernate-mapping package="test.generate.entity">
    <class name="Test5">

        <composite-id name="test5Id" class="test.generate.entity.Test5Id">
            <key-property name="id"/>
            <key-property name="name"/>
        </composite-id>

        <timestamp name="version"/>
<!-- 
        <one-to-one name="test6" cascade="all" lazy="proxy"/>
 -->
        <many-to-one name="test6"
                insert="false" update="false"
                cascade="all">
        	<column name="id" unique="true"/>
        	<column name="name" unique="true"/>
        </many-to-one>
        
    </class>
</hibernate-mapping>
<hibernate-mapping package="test.generate.entity">
    <class name="Test6">
    
        <composite-id name="test6Id" class="test.generate.entity.Test5Id">
            <key-property name="id"/>
            <key-property name="name"/>
        </composite-id>
        
        <timestamp name="version"/>
        
        <one-to-one name="test5" constrained="true" lazy="proxy"/>

    </class>
</hibernate-mapping>

複合キーなのでuniqueは外した方がいいのかな?・・・まぁとりあえずこのままで(笑)
で、早速実行

Hibernate: 
    /* insert test.generate.entity.Test6
        */ insert 
        into
            Test6
            (version, id, name) 
        values
            (?, ?, ?)
WARN  2006-02-01 00:02:45,531 [main] SQL Error: 0, SQLState: null
ERROR 2006-02-01 00:02:45,531 [main] failed batch
ERROR 2006-02-01 00:02:45,609 [main] Could not synchronize database state with session
org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
	at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:91)
	at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:79)
	at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:43)
	at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:202)
	at org.hibernate.jdbc.AbstractBatcher.prepareStatement(AbstractBatcher.java:91)
	at org.hibernate.jdbc.AbstractBatcher.prepareStatement(AbstractBatcher.java:86)
	at org.hibernate.jdbc.AbstractBatcher.prepareBatchStatement(AbstractBatcher.java:171)
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2048)
	at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2427)
	at org.hibernate.action.EntityInsertAction.execute(EntityInsertAction.java:51)
	at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:243)
	at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:227)
	at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:140)
	at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:296)
	at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
	at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1007)
	at test.entitytest.Main3.main(Main3.java:43)
Caused by: java.sql.BatchUpdateException: failed batch
	at org.hsqldb.jdbc.jdbcStatement.executeBatch(Unknown Source)
	at org.hsqldb.jdbc.jdbcPreparedStatement.executeBatch(Unknown Source)
	at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:58)
	at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:195)
	... 13 more
INFO  2006-02-01 00:02:45,640 [main] closing
INFO  2006-02-01 00:02:45,640 [main] cleaning up connection pool: jdbc:hsqldb:hsql://localhost

・・・うーむ・・・
CASCADEで登録していた場合、今までは、外部キー制約を持ってる方のテーブルがエラーにならないよう、HibernateがINSERTする順番を考えてくれてたみたいなんですが、それがうまくいかないみたいです。
どうやら、本来は連携した1対1で定義されている筈が、多対1に見せかけてる関係で、外部キーに関連する登録の順番を意識して、Sessionにsaveしてあげないといけないみたいですね。・・・まぁでも、外部キーで参照される方を最初に永続化してあげれば済む話か・・・
というわけで、実行クラスのメソッドを少しだけ変更

			Test5 test5 = new Test5();
			Test5Id test5Id = new Test5Id();
			test5Id.setId(8);
			test5Id.setName("TEST5");
			test5.setTest5Id(test5Id);
			session.save(test5);

			Test6 test6 = new Test6();
			test6.setTest6Id(test5Id);
			test6.setTest5(test5);
			test5.setTest6(test6);
			
//			session.save(test5);

CASCADEの親であるTest5を先に永続化しておいて、そのオブジェクトに後からTest6を追加します。主キーで結合してるから、特にTest5に対するUPDATEは起こらない筈。
これで再度実行・・・

Hibernate: 
    /* insert test.generate.entity.Test5
        */ insert 
        into
            Test5
            (version, id, name) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert test.generate.entity.Test6
        */ insert 
        into
            Test6
            (version, id, name) 
        values
            (?, ?, ?)
Hibernate: 
    /* load test.generate.entity.Test5 */ select
        test5x0_.id as id4_0_,
        test5x0_.name as name4_0_,
        test5x0_.version as version4_0_ 
    from
        Test5 test5x0_ 
    where
        test5x0_.id=? 
        and test5x0_.name=?
TEST5.ID:8
TEST5.NAME:TEST5
TEST5.VERSION:2006-02-01 00:08:11.671

TEST6.ID:8
TEST6.NAME:TEST5
Hibernate: 
    /* load test.generate.entity.Test6 */ select
        test6x0_.id as id5_0_,
        test6x0_.name as name5_0_,
        test6x0_.version as version5_0_ 
    from
        Test6 test6x0_ 
    where
        test6x0_.id=? 
        and test6x0_.name=?
TEST6.VERSION:2006-02-01 00:08:11.703

Hibernate: 
    /* load test.generate.entity.Test6 */ select
        test6x0_.id as id5_0_,
        test6x0_.name as name5_0_,
        test6x0_.version as version5_0_ 
    from
        Test6 test6x0_ 
    where
        test6x0_.id=? 
        and test6x0_.name=?
TEST6.ID:8
TEST6.NAME:TEST5
TEST6.VERSION:2006-02-01 00:08:11.703

TEST5.ID:8
TEST5.NAME:TEST5
Hibernate: 
    /* load test.generate.entity.Test5 */ select
        test5x0_.id as id4_0_,
        test5x0_.name as name4_0_,
        test5x0_.version as version4_0_ 
    from
        Test5 test5x0_ 
    where
        test5x0_.id=? 
        and test5x0_.name=?
TEST5.VERSION:2006-02-01 00:08:11.671

INFO  2006-02-01 00:08:11,875 [main] closing
INFO  2006-02-01 00:08:11,875 [main] cleaning up connection pool: jdbc:hsqldb:hsql://localhost

おぉ、きれいにLAZYロードされました!
結合するID情報は既に持っているから、ID以外の情報を呼び出すときに初めてSQL文が発行されているのがわかります。これで、本当に必要になるまで関連クラスのSQL文は発行されずに済みます。これがやりたかったんですよね!
永続化するポイントに若干の制限が入りますが、カスケードも効くし、これでほぼ問題は解決したと言えそうです・・・Hibernateに関しては。
さて、これを使って、何とかHibernate EntityManagerに対しても、1対1のLAZYロードを使いたいものですが・・・問題は、現時点では、複合主キーを使った@OneToOneはたとえ単一方向の関連設定のときでもLAZYロードが効かないということ。これがクリア出来なければ、問題は解決できません・・・
・・・でも良く考えると、Hibernateの例の時点で、既に双方向同士の関連設定は独立したものになってしまっているから・・・つまり、両方@ManyToOneにしてしまえばいいのかな?(汗)
というわけで、早速試してみます。
先ほど動かしたクラスを、今度はJPAアノテーションで定義し直します。

/**
 * Test5 generated by hbm2java
 */
@Entity
public class Test5 implements Serializable {

	// Fields

	/**
	 * 
	 */
	private static final long serialVersionUID = -4004185336469821692L;

	@EmbeddedId
	private Test5Id test5Id;

	@Version
	private Timestamp version;

	@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
	@JoinColumns({
		@JoinColumn(name = "id", referencedColumnName = "id", insertable = false, updatable = false),
		@JoinColumn(name = "name", referencedColumnName = "name", insertable = false, updatable = false)
	})
	private Test6 test6;

(setter、getter略)

}
/**
 * Test6 generated by hbm2java
 */
@Entity
public class Test6 implements Serializable {

	// Fields

	/**
	 * 
	 */
	private static final long serialVersionUID = 5904589021008814390L;

	@EmbeddedId
	private Test5Id test6Id;

	@Version
	private Timestamp version;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumns({
		@JoinColumn(name = "id", referencedColumnName = "id", insertable = false, updatable = false),
		@JoinColumn(name = "name", referencedColumnName = "name", insertable = false, updatable = false)
	})
	private Test5 test5;

(setter、getter略)

}

どちらにも、同じ設定で@ManyToOneを定義しました。複合キーなので、@JoinColumnのuniqueは外しました。
さて、これを動かす実行クラスは・・・

public class Test3ServiceImpl implements Test3Service {

	public static void main(String[] args) {
		SingletonS2ContainerFactory.init();
		S2Container container = SingletonS2ContainerFactory.getContainer();
		Test3Service service = (Test3Service) container
				.getComponent(Test3Service.class);
		service.execute();
	}

	private EntityManager em;

	public void setEm(EntityManager em) {
		this.em = em;
	}

	/*
	 * (非 Javadoc)
	 * 
	 * @see test.entitytest.Test2Service#execute()
	 */
	/* (非 Javadoc)
	 * @see test.entitytest.Test3Service#execute()
	 */
	public void execute() {
		Test5 test5 = new Test5();
		Test5Id id = new Test5Id();
		id.setId(9);
		id.setName("TEST");
		test5.setTest5Id(id);
		em.persist(test5);
		
		Test6 test6 = new Test6();
		test6.setTest6Id(id);
		test6.setTest5(test5);
		test5.setTest6(test6);
		
		em.flush();
		em.clear();

		test5 = (Test5) em.find(Test5.class, test5.getTest5Id());
		System.out.println("TEST5.ID:" + test5.getTest5Id().getId());
		System.out.println("TEST5.NAME:" + test5.getTest5Id().getName());
		System.out.println("TEST5.VERSION:" + test5.getVersion());
		System.out.println();
		test6 = test5.getTest6();
		System.out.println("TEST6.ID:" + test6.getTest6Id().getId());
		System.out.println("TEST6.NAME:" + test6.getTest6Id().getName());
		System.out.println("TEST6.VERSION:" + test6.getVersion());
		System.out.println();

		em.clear();

		test6 = (Test6) em.find(Test6.class, test6.getTest6Id());
		System.out.println("TEST6.ID:" + test6.getTest6Id().getId());
		System.out.println("TEST6.NAME:" + test6.getTest6Id().getName());
		System.out.println("TEST6.VERSION:" + test6.getVersion());
		System.out.println();
		test5 = test6.getTest5();
		System.out.println("TEST5.ID:" + test5.getTest5Id().getId());
		System.out.println("TEST5.NAME:" + test5.getTest5Id().getName());
		System.out.println("TEST5.VERSION:" + test5.getVersion());
		System.out.println();

	}
}

Hibernateの実行クラスをS2Hibernate-JPA環境で置き換えたものです。executeメソッドにはログとトランザクションアスペクトをかけてます。
早速これで実行・・・

DEBUG 2006-02-01 00:23:37,281 [main] BEGIN test.entitytest.Test3ServiceImpl#execute()
DEBUG 2006-02-01 00:23:37,296 [main] トランザクションを開始しました
DEBUG 2006-02-01 00:23:37,468 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.Test5
        */ insert 
        into
            Test5
            (version, id, name) 
        values
            (?, ?, ?)
DEBUG 2006-02-01 00:23:37,484 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-01 00:23:37,484 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.Test6
        */ insert 
        into
            Test6
            (version, id, name) 
        values
            (?, ?, ?)
DEBUG 2006-02-01 00:23:37,484 [main] 論理的なコネクションを閉じました
DEBUG 2006-02-01 00:23:37,500 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Test5 */ select
        test5x0_.id as id4_0_,
        test5x0_.name as name4_0_,
        test5x0_.version as version4_0_ 
    from
        Test5 test5x0_ 
    where
        test5x0_.id=? 
        and test5x0_.name=?
DEBUG 2006-02-01 00:23:37,546 [main] 論理的なコネクションを閉じました
TEST5.ID:9
TEST5.NAME:TEST
TEST5.VERSION:2006-02-01 00:23:37.421

DEBUG 2006-02-01 00:23:37,750 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Test6 */ select
        test6x0_.id as id2_0_,
        test6x0_.name as name2_0_,
        test6x0_.version as version2_0_ 
    from
        Test6 test6x0_ 
    where
        test6x0_.id=? 
        and test6x0_.name=?
DEBUG 2006-02-01 00:23:37,765 [main] 論理的なコネクションを閉じました
TEST6.ID:9
TEST6.NAME:TEST
TEST6.VERSION:2006-02-01 00:23:37.437

DEBUG 2006-02-01 00:23:37,765 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Test6 */ select
        test6x0_.id as id2_0_,
        test6x0_.name as name2_0_,
        test6x0_.version as version2_0_ 
    from
        Test6 test6x0_ 
    where
        test6x0_.id=? 
        and test6x0_.name=?
DEBUG 2006-02-01 00:23:37,765 [main] 論理的なコネクションを閉じました
TEST6.ID:9
TEST6.NAME:TEST
TEST6.VERSION:2006-02-01 00:23:37.437

DEBUG 2006-02-01 00:23:37,812 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Test5 */ select
        test5x0_.id as id4_0_,
        test5x0_.name as name4_0_,
        test5x0_.version as version4_0_ 
    from
        Test5 test5x0_ 
    where
        test5x0_.id=? 
        and test5x0_.name=?
DEBUG 2006-02-01 00:23:37,812 [main] 論理的なコネクションを閉じました
TEST5.ID:9
TEST5.NAME:TEST
TEST5.VERSION:2006-02-01 00:23:37.421

DEBUG 2006-02-01 00:23:37,812 [main] トランザクションをコミットしました
DEBUG 2006-02-01 00:23:37,812 [main] END test.entitytest.Test3ServiceImpl#execute() : null

・・・Hibernateのときと、微妙に呼び出されるタイミングが異なりますが(汗)・・・まぁよしとしましょう(笑)
とりあえず、ちょっと強引な気もしますが、主キーを使った1対1の関連は、それぞれを@ManyToOneで定義することで、双方向のLAZYロードが実現できそうですね。ふぅ・・・これで一安心。
ちなみに、ユニークな外部キーを使って1対1の定義を行っている場合は、Hibernateと同様に、@ManyToOneと@OneToOneの組み合わせで同様の内容が実現可能でした。どの場合にも、やはりテーブルの外部キー制約に引っかからないように、外部キー参照される方の永続化クラスを先にpersistしておく、ということが必要のようです。まぁ、LAZYロードが出来なかったり、双方向定義が出来ない問題に比べれば、微々たる制限ではないかと思います。
とりあえず一つ問題をクリア。これで何とか、全体的なテーブル関連定義のマッピング手法がとれそうです。
(追記)一応EJB-QLのFETCH JOINを試してみましたが、特に問題なく動きました。とりあえずこの設定で使っていこうと思います。
あと念の為、・・・この処理はおそらくHibernateに強く依存した方法だと思うので、他のJPA実装では当てはまらない内容だと思います。今回の制限自体が、Hibernate実装に関する内容でしたし。