普通には lazy フェッチできないケース(LoadTimeWeaverで対応)

前回、以下のアノテーションにより、対1関連をフェッチできると書きました。

@OneToOne(fetch=FetchType.LAZY)

しかし、単純にそうもいかないケースもあるとも書きました。

これらのケースを、例を追いながら説明し、その後 LoadTimeWeaver を用いた解決方法を示します。

例1

概要
  • クラス A と クラス B が互いに 1対1関連を持っている。
  • DB のテーブル上、テーブル A はテーブル B の外部キーを持っているが、テーブル B はテーブル A への外部キーを持っていない。
ソース

クラス A

@Entity
public class A extends EntityBase {
	@OneToOne(fetch=FetchType.LAZY)
	private B b;
	public B getB() { return b; }
	public void setB(B b) { this.b = b; }
}

クラス B

@Entity
public class B extends EntityBase {
	@OneToOne(mappedBy="b", fetch=FetchType.LAZY)
	private A a;
	public A getA() { return a; }
	public void setA(A a) { this.a = a; }
}

EntityBase

@MappedSuperclass
public class EntityBase {
	@Id @GeneratedValue
	private int id;
}
うまくいくケース(A の取得)
A a = em.find(A.class, 1);

// lazyロード成功!
// select a0_.id as id0_0_, a0_.b_id as b2_0_0_ from A a0_ where a0_.id=1
うまくいかないケース(B の取得)
B b = em.find(B.class, 1);

// lazyロード失敗
// select b0_.id as id1_0_ from B b0_ where b0_.id=1
// select a0_.id as id0_0_, a0_.b_id as b2_0_0_ from A a0_ where a0_.b_id=1

なぜうまくいかないケースが存在するのか?

このケースに関する基本的な考え方は、に Some explanations on lazy loading わかりやすく説明されています。

上記の例の場合は、取得した B のインスタンスの a フィールドに対して、

  • 対応する a が存在するならばプロキシを配置
  • 対応する a が存在しないならば null を配置

したいわけですが、その存在チェックのために a のテーブルの対象レコードを検索したら、それはすでに lazy ではないということになってしまいます。

SpringFramework を用いた Not EJB 環境の解決策

EntityManager の設定

EntityManager として、LocalContainerEntityManagerFactoryBean を使用します。

<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" />

Spring2.0系の場合は、LoadTimeWeaver も明示的に指定します。

<property name="loadTimeWeaver">
  <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/>
</property>
persistence.xml の設定

Enhancserを有効にします。

<property name="hibernate.ejb.use_class_enhancer" value="true" />
スタンドアロン環境の場合

java vm の起動設定に、spring-agent を使用します。

-javaagent:\spring-agent-2.5.4.jar
tomcat環境の場合

Context の Loader に、TomcatInstrumentableClassLoader を指定します。

<Context path="/myWebApp" docBase="/my/webApp/location">
    <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
</Context>
B のソースに @LazyToOne アノテーションを追加

クラス B のソース上で、A への関連について @LazyToOne アノテーションを追加します。オプションを LazyToOneOption.NO_PROXY に設定することにより、プロキシではなくバイトコードの加工が行われます。

@Entity
public class B extends EntityBase {
	@OneToOne(mappedBy="b", fetch=FetchType.LAZY)
	@LazyToOne(LazyToOneOption.NO_PROXY)
	private A a;
	public A getA() { return a; }
	public void setA(A a) { this.a = a; }
}

SpringFramework を用いた Not EJB 環境の実行例

うまくいくケース(A の取得)
A a = em.find(A.class, 1);

// lazyロード成功!
// select a0_.id as id0_0_, a0_.b_id as b2_0_0_ from A a0_ where a0_.id=1

// もし A 側にも  @LazyToOne アノテーションを追加した場合、下記のようなクエリが発行されます。
// select a0_.id as id0_0_ from A a0_ where a0_.id=1
うまくいくケース(B の取得)
B b = em.find(B.class, 1);

// lazyロード成功
// select b0_.id as id1_0_ from B b0_ where b0_.id=1

参考

サンプル