ToplinkのLazy Loading
HIBERNATE イン アクションのp.290あたりに説明があるように、HibernateはManyToOneでポリモーフックな関連をLazy Loadするときにプロキシを使ってます。Toplinkってどうなんだろうと妙に気になるのでちょっと動かしてみました。
結論から言うと、プロキシになるところがHibernateとは違ってました。A と B にManyToOneの関係があるとするとHibernateではBがプロキシになりますが、ToplinkではAがプロキシになる(エンハンスされる?)みたいです。
http://blogs.sun.com/roller/page/marina?entry=testing_standalone_java_persistence_bundle
Toplinkはここを参考にして動かしました。つかったbuildは「build 47」。
DDL:HIBERNATE イン アクションにでてくるのを簡略化したかんじで。エンティティクラスの継承にはJOINED Subclassを使いますが、Hibernateと違ってToplinkはJOINED SubclassのときにDiscriminatorColumnを使うようなのでBillingDetailsテーブルにDTYPEカラムを持たせてあります。
create table BillingDetails ( ID INTEGER PRIMARY KEY, DTYPE VARCHAR(31), NUMBER VARCHAR(10) ); create table CreditCard ( ID INTEGER PRIMARY KEY ); create table User ( ID INTEGER PRIMARY KEY, BILLINGDETAILS_ID INTEGER NOT NULL, FOREIGN KEY (BILLINGDETAILS_ID) REFERENCES BILLINGDETAILS (ID) );
データを作っておきます。
insert into BillingDetails values(1, 'CreditCard', '2010') insert into CreditCard values(1) insert into User values(1,1)
エンティティ:User。BillingDetails をLazyで取得します。
@Entity
public class User {
@Id
private int id;
@ManyToOne(fetch=FetchType.LAZY)
private BillingDetails billingDetails;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public BillingDetails getBillingDetails() {
return billingDetails;
}
public void setBillingDetails(BillingDetails billingDetails) {
this.billingDetails = billingDetails;
}
}
エンティティ:BillingDetails。継承戦略使います。このクラスでは@DiscriminatorColumnを指定してないのでDefaultのDTYPEがDiscriminatorColumnとなります。
@Entity
@Inheritance(strategy=JOINED)
public class BillingDetails {
@Id
private int id;
private String number;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
}
エンティティ、CreditCard :BillingDetailsを継承します。
@Entity public class CreditCard extends BillingDetails { }
persistence.xml:明示的にクラスを指定しないでもclasspathから自動的にエンティティをみつけてくれたらいいのになぁ。あとddl-generationというプロパティを使ってみましたが効きませんでした。アプリケーションサーバー経由限定なのかな?
(追記: リンク先の説明どおりにしたらテーブルの自動生成できました。http://blogs.sun.com/roller/page/java2dbInGlassFish/20051219)
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="emf"> <provider>oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider</provider> <class>entity.User</class> <class>entity.BillingDetails</class> <class>entity.CreditCard</class> <properties> <property name="toplink.jdbc.driver" value="org.hsqldb.jdbcDriver"/> <property name="toplink.jdbc.url" value="jdbc:hsqldb:hsql://localhost"/> <property name="toplink.jdbc.user" value="sa"/> <property name="toplink.jdbc.password" value=""/> <property name="toplink.platform.class.name" value="oracle.toplink.essentials.platform.database.HSQLPlatform"/> <property name="toplink.logging.level" value="FINE"/> </properties> </persistence-unit> </persistence>
実行クラス:user.getBillingDetails()する時点でSQLが発行されてBillingDetailsが取得されます。
public class Client { private static EntityManagerFactory emf; private static EntityManager em; public static void main(String[] args) throws Exception { emf = Persistence.createEntityManagerFactory("emf"); createTransactionalEntityManager(); User user = em.find(User.class, 1); Field f = user.getClass().getDeclaredField("billingDetails"); f.setAccessible(true); System.out.println(f.get(user) == null); // true BillingDetails bd = user.getBillingDetails(); System.out.println(bd instanceof CreditCard); // true System.out.println(f.get(user) == null); // false closeTransactionalEntityManager(); } private static void createTransactionalEntityManager() { em = emf.createEntityManager(); em.getTransaction().begin(); } private static void closeTransactionalEntityManager() { em.getTransaction().commit(); em.close(); } }
Hibernateの場合、user.getBillingDetails()で返されるオブジェクトがプロキシになっているので bd instanceof CreditCard は false になるのですが、Toplinkでは trueになります。ステップ実行するとuser.getBillingDetails()から_toplink_getBillingDetails()というメソッドが呼び出されているのでUserがエンハンスされているみたいです。usre instanceof User としても trueになりますけど...。CGLIBやjavassistのようにサブクラス化されるわけではないのかな?。
ちなみにフィールドに直接アクセスしてみましたが、user.getBillingDetails()を呼ばない限りはLoadしないみたいです。