Hibernate EntityManagerその3

もう毎度のことになりつつありますが、またまた自分は間違ってました(苦笑)
Hibernate EntityManagerのリファレンスに

The configuration for entity managers both inside an application server and in a standalone application reside in a persistence archive (.par). A persistence archive is a JAR file with the .par suffix instead of .jar. You must also define a persistence.xml file that resides in the META-INF folder of the .par file. All properly annotated classes included in the par archive (ie having an @Entity annotation), all annotated packages and all Hibernate hbm.xml files included in the par will be added to the persistence unit configuration, so by default, your persistence.xml will be quite minimalist:

こう書いてあったので、てっきりjarを作らないと動かないものだとばかり思っていたのですが・・・
試しに、Eclipseのソースフォルダ上にMETA-INFディレクトりを作成し、その中に以下のようなpersistence.xmlを作ってみました。

<entity-manager>
   <name>test</name>
   <jta-data-source>j2ee.dataSource</jta-data-source>
   <properties>
      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
      <property name="hibernate.jndi.class" value="org.seasar.extension.j2ee.JndiContextFactory"/>
      <property name="hibernate.transaction.manager_lookup_class" value="test.hibernate.S2TransactionManagerLookup"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="hibernate.use_sql_comments" value="true"/>
   </properties>
</entity-manager>

S2のJndiContextによって、HibernateにDataSourceとTransactionManagerを渡すのは前回通りです。また、TransactionManagerLookupインターフェイスは、以下のように実装しました。

package test.hibernate;

import org.hibernate.transaction.JNDITransactionManagerLookup;

public class S2TransactionManagerLookup extends JNDITransactionManagerLookup {

	private static final String TRANSACTION_MANAGER_NAME = "j2ee.transactionManager";
	@Override
	protected String getName() {
		return TRANSACTION_MANAGER_NAME;
	}

	public String getUserTransactionName() {
		return TRANSACTION_MANAGER_NAME;
	}

}

TransactionManagerのComponent名を返すだけのクラスです。前回の日記では、ComponentAutoRegisterを使ってAnnotationConfigurationを作成していましたが、今回はEJB3正式のやり方でEntityManagerを作成してみます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.3//EN" 
	"http://www.seasar.org/dtd/components23.dtd">
<components namespace="hibernate">

	<component class="javax.persistence.EntityManager">
	<aspect>
	<component class="org.seasar.framework.aop.interceptors.DelegateInterceptor">
		<initMethod name="setTarget">
		<arg>
		@javax.persistence.Persistence@createEntityManagerFactory("test").entityManager
		</arg>
		</initMethod>
	</component>
	</aspect>
	</component>

</components>

テスト用クラスは、前回同様Hibernate Toolsプラグインの自動作成機能を使って作成した、HSQLDBデモテーブルのEntityBean。それを使うClientは以下のようになります。

package test.service;

import java.math.BigDecimal;
import java.util.List;

import javax.persistence.EntityManager;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;

import test.generate.entity.Invoice;

public class TestServiceImpl implements TestService {
	
	private EntityManager em;
	
	public void setEm(EntityManager em) {
		this.em = em;
	}

	public static void main(String[] args) {
		SingletonS2ContainerFactory.init();
		S2Container container = SingletonS2ContainerFactory.getContainer();

		TestService service = (TestService) container.getComponent(TestService.class);
		service.execute();
	}

	public void execute() {
		
		List<Invoice> list = (List<Invoice>) em.createQuery(
			"SELECT iv FROM Invoice iv INNER JOIN FETCH iv.customer WHERE iv.total >= :total")
			.setParameter("total", new BigDecimal("6000"))
			.getResultList();
		for (Invoice iv : list) {
			System.out.println("INVOICE_ID:" + iv.getId());
			System.out.println("FIRSTNAME:" + iv.getCustomer().getFirstname());
			System.out.println("LASTNAME:" + iv.getCustomer().getLastname());
			System.out.println("CITY:" + iv.getCustomer().getCity());
			System.out.println("STREET:" + iv.getCustomer().getStreet());
			System.out.println("TOTAL:" + iv.getTotal());
			System.out.println();
			iv.setTotal(iv.getTotal().add(new BigDecimal("100")));
		}
	}

}

↑のクラスには、ログとトランザクションアスペクトをかけています。
さて、これらのクラスをJarに固めないまま実行してみると・・・

DEBUG 2005-12-17 00:02:40,953 [main] BEGIN test.service.TestServiceImpl#execute()
DEBUG 2005-12-17 00:02:40,953 [main] トランザクションを開始しました
DEBUG 2005-12-17 00:02:41,484 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* SELECT
        iv 
    FROM
        Invoice iv 
    INNER JOIN
        FETCH iv.customer 
    WHERE
        iv.total >= :total */ select
            invoice0_.ID as ID1_0_,
            customer1_.ID as ID0_1_,
            invoice0_.CUSTOMERID as CUSTOMERID1_0_,
            invoice0_.TOTAL as TOTAL1_0_,
            customer1_.FIRSTNAME as FIRSTNAME0_1_,
            customer1_.LASTNAME as LASTNAME0_1_,
            customer1_.CITY as CITY0_1_,
            customer1_.STREET as STREET0_1_ 
        from
            PUBLIC.INVOICE invoice0_ 
        inner join
            PUBLIC.CUSTOMER customer1_ 
                on invoice0_.CUSTOMERID=customer1_.ID 
        where
            invoice0_.TOTAL>=?
DEBUG 2005-12-17 00:02:41,531 [main] 論理的なコネクションを閉じました
INVOICE_ID:9
FIRSTNAME:Andrew
LASTNAME:Heiniger
CITY:Lyon
STREET:347 College Av.
TOTAL:6982.20

INVOICE_ID:13
FIRSTNAME:Mary
LASTNAME:Karsen
CITY:Chicago
STREET:202 College Av.
TOTAL:7001.70

INVOICE_ID:37
FIRSTNAME:Mary
LASTNAME:Karsen
CITY:Chicago
STREET:202 College Av.
TOTAL:7331.10

INVOICE_ID:38
FIRSTNAME:Andrew
LASTNAME:May
CITY:New York
STREET:172 Seventh Av.
TOTAL:6373.80

DEBUG 2005-12-17 00:02:41,609 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* update
        test.generate.entity.Invoice */ update
            PUBLIC.INVOICE 
        set
            CUSTOMERID=?,
            TOTAL=? 
        where
            ID=?
Hibernate: 
    /* update
        test.generate.entity.Invoice */ update
            PUBLIC.INVOICE 
        set
            CUSTOMERID=?,
            TOTAL=? 
        where
            ID=?
Hibernate: 
    /* update
        test.generate.entity.Invoice */ update
            PUBLIC.INVOICE 
        set
            CUSTOMERID=?,
            TOTAL=? 
        where
            ID=?
Hibernate: 
    /* update
        test.generate.entity.Invoice */ update
            PUBLIC.INVOICE 
        set
            CUSTOMERID=?,
            TOTAL=? 
        where
            ID=?
DEBUG 2005-12-17 00:02:41,625 [main] 論理的なコネクションを閉じました
DEBUG 2005-12-17 00:02:41,625 [main] トランザクションをコミットしました
DEBUG 2005-12-17 00:02:41,625 [main] END test.service.TestServiceImpl#execute() : null

・・・普通に実行出来てしまいました(汗)・・・
つまり、Hibernate EntityManagerはJar上でもファイル上でも、META-INF/persistence.xmlが存在する同一クラスパス上のEntityBeanを自動的に検索して登録してくれるってことか。何気に凄い・・・
Connectionについては、Hibernateの資料を読むと、JTAトランザクションの場合は、SQLが実行された直後にクローズしてくれるみたいですね。そして・・・flushについては、TransactionManagerと連携して、コミットのタイミングで行われているみたいです。試しにpersistence.xmlの「hibernate.transaction.manager_lookup_class」の記述をコメントアウトして実行してみると・・・

DEBUG 2005-12-17 00:16:08,640 [main] BEGIN test.service.TestServiceImpl#execute()
DEBUG 2005-12-17 00:16:08,640 [main] トランザクションを開始しました
DEBUG 2005-12-17 00:16:09,265 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* SELECT
        iv 
    FROM
        Invoice iv 
    INNER JOIN
        FETCH iv.customer 
    WHERE
        iv.total >= :total */ select
            invoice0_.ID as ID1_0_,
            customer1_.ID as ID0_1_,
            invoice0_.CUSTOMERID as CUSTOMERID1_0_,
            invoice0_.TOTAL as TOTAL1_0_,
            customer1_.FIRSTNAME as FIRSTNAME0_1_,
            customer1_.LASTNAME as LASTNAME0_1_,
            customer1_.CITY as CITY0_1_,
            customer1_.STREET as STREET0_1_ 
        from
            PUBLIC.INVOICE invoice0_ 
        inner join
            PUBLIC.CUSTOMER customer1_ 
                on invoice0_.CUSTOMERID=customer1_.ID 
        where
            invoice0_.TOTAL>=?
DEBUG 2005-12-17 00:16:09,312 [main] 論理的なコネクションを閉じました
INVOICE_ID:9
FIRSTNAME:Andrew
LASTNAME:Heiniger
CITY:Lyon
STREET:347 College Av.
TOTAL:7082.20

INVOICE_ID:13
FIRSTNAME:Mary
LASTNAME:Karsen
CITY:Chicago
STREET:202 College Av.
TOTAL:7101.70

INVOICE_ID:37
FIRSTNAME:Mary
LASTNAME:Karsen
CITY:Chicago
STREET:202 College Av.
TOTAL:7431.10

INVOICE_ID:38
FIRSTNAME:Andrew
LASTNAME:May
CITY:New York
STREET:172 Seventh Av.
TOTAL:6473.80

DEBUG 2005-12-17 00:16:09,343 [main] トランザクションをコミットしました
DEBUG 2005-12-17 00:16:09,343 [main] END test.service.TestServiceImpl#execute() : null

flushが行われずにコミットされてしまってます。また、遅延ロード機能をトランザクションマネージャーが見えない環境で使おうとすると、

2005-12-17 00:18:21,312 [main] ERROR org.hibernate.LazyInitializationException - failed to lazily initialize a collection of role: test.generate.entity.Invoice.items, no session or session was closed
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: test.generate.entity.Invoice.items, no session or session was closed
	at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:358)
	at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:350)
	at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:343)
	at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:86)
	at org.hibernate.collection.PersistentSet.iterator(PersistentSet.java:138)
	at test.service.TestServiceImpl.execute(TestServiceImpl.java:43)
	at test.service.TestServiceImpl$$EnhancedByS2AOP$$2b7db1.execute$$invokeSuperMethod$$(TestServiceImpl$$EnhancedByS2AOP$$2b7db1.java)
	at test.service.TestServiceImpl$$EnhancedByS2AOP$$2b7db1$$MethodInvocation$$execute0.proceed(MethodInvocationClassGenerator.java)
	at org.seasar.extension.tx.RequiredInterceptor.invoke(RequiredInterceptor.java:40)
	at test.service.TestServiceImpl$$EnhancedByS2AOP$$2b7db1$$MethodInvocation$$execute0.proceed(MethodInvocationClassGenerator.java)
	at org.seasar.framework.aop.interceptors.TraceInterceptor.invoke(TraceInterceptor.java:50)
	at test.service.TestServiceImpl$$EnhancedByS2AOP$$2b7db1$$MethodInvocation$$execute0.proceed(MethodInvocationClassGenerator.java)
	at test.service.TestServiceImpl$$EnhancedByS2AOP$$2b7db1.execute(TestServiceImpl$$EnhancedByS2AOP$$2b7db1.java)
	at test.service.TestServiceImpl.main(TestServiceImpl.java:26)
DEBUG 2005-12-17 00:18:21,328 [main] トランザクションをロールバックしました

LazyInitializationExceptionが吹っ飛んできます。JTA EntityManagerに関しては、TransactionManagerと連携して自動でflushを行ってくれているってことですね。
という訳で・・・昨日自分がゴニョゴニョ作っていたクラス関連は、殆ど必要なくなりました(汗)Hibernate EntityManagerさえあれば、persistence.xmlにDataSourceとTransactionManagerの設定をしてあげるだけで、簡単にコンテナ管理EntityManagerを作成することが出来るんですね。正直、標準の方法で、こんなに簡単に環境が構築出来るとは思ってませんでした。最初から本来のやり方で試すべきだった・・・
もうHibernateには、マッピング用ファイルも登録用ファイルも要らないんですね。しかもPersistence API標準の方法でEntityManagerを作成出来るから、もし将来、実装を他のフレームワークに変更したとしても、persistence.xmlの詳細設定を変えるだけでほぼ同じように利用することが出来る筈。たとえJTA環境を持ってなくても、ここまで設定が簡単なら、JDBCを使う感覚で積極的にEntityManagerを使っていっても問題無いのではないでしょうか。