Hibernate EntityManagerその2

以前、ローカル環境で簡単にEJB3を試したくて、Hibernate EntityManagerを適当に修正して動かして遊んでいたのですが・・・ちょっと真面目に勉強する必要が出てきてしまいました(汗)前回の自分の処理は、今思えばあまりに適当だったので・・・ここで、動作環境から作り直すことにしました。
HibernateでいうとSessionFactoryにあたる、EntityManagerというインターフェイスがあります。このインターフェイスEJB3のEntityBeanの中心的存在になるみたいですね。このEntityManagerは、Java EE環境ではコンテナにトランザクションをお任せできるそうです。しかし、Java SE環境ではアプリケーションベースのトランザクションでしか利用出来ないんだとか・・・DIコンテナを使った宣言トランザクションに慣れきってしまった現在、今更commitやrollbackを記述する気にはなれません。ただし、EJB3環境を構築するのは面倒だし、Jarにまとめずに簡単に動かしてみたいです。・・・なので、とりあえずの目標を「ローカル環境で、コンテナ(DIコンテナのアスペクト)にトランザクションをお任せした状態で動く、Hibernate EntityManager環境を構築する」としたいと思います。
HibernateのEntityManager実装には、EntityManagerImplとCurrentEntityManagerImplの2種類がありました。このクラスを利用しているのは、EntityManagerFactoryImplクラスでした。このFactoryクラスは、getEntityManagerメソッドではCurrentEntityManagerImplオブジェクトを作成し、createEntityManagerメソッドではEntityManagerImplオブジェクトを作成していました。EJB3関連資料によると、JTA EntityManagerになるのは、getEntityManagerメソッドを使って取得したときなんだとか・・・つまり、今回利用したいのは、このCurrentEntityManagerImplクラスということですね。
ソースを見てみると、コンストラクタでSessionFactoryオブジェクトを受け取っています。ほとんどの処理はこのSessionFactoryに委譲しています。つまり、初期化処理でSessionFactoryを作成してそのインスタンスをコンストラクタインジェクションで渡してやれば、DIコンテナに登録することが可能ということか。DIコンテナに登録さえできれば、もうFactoryクラスは必要無くなるので、EntityManagerFactoryの実装クラスについて考える必要は無さそうです。
ただ・・・EntityManager実装クラスのソースを眺めていると、HibernateJTA連携機能を内部で利用しているところが数箇所発見されました。トランザクションの存在チェックや、EntityTransactionオブジェクトの作成に利用されています。今まで自分はS2Hibernate環境でしかHibernateを触ったことが無くて、HibernateJTA連携機能は使ったこと無かったのですが・・・Hibernate EntityManagerを利用するには、このJTA連携機能を利用するしか無さそうです。
Hibernate3.1のリファレンスのプロパティ設定を読んでいたところ、DataSourceとTransactionManagerを、通常はJNDIを使って取得するらしいことがわかりました。ってことは、JNDIからDIコンテナのコンポーネントが取得出来ればいいのか・・・
とりあえず、Contextインターフェイスの実装クラスをEclipseで検索してみたところ、Seasarのライブラリの中にJndiContextというクラスを発見しました。これは、lookupメソッドで渡した文字列をキーに、DIコンテナからコンポーネントを取得してくれるというクラスでした。おぉ、こんなクラスがあったのか・・・全然知りませんでした(汗)EJB2時代のシステム等の連携とか、いろいろ便利に使えそうなクラスですね。
このJndiContextを返す、InitialContextFactoryの実装クラスもあったので、このクラス名をHibernateのJNDI関連設定に渡してやれば、Hibernateに対してS2のDataSourceとTransactionManagerを渡してやれそうです。・・・ただ、TransactionManagerについては、JNDI名を指定する為にはLookup用クラスを定義してやらなければいけないみたいです。ただ、Stringを返すだけのクラスなので、とりあえず"j2ee.transactionManager"と、diconファイルのトランザクションマネージャー名を返すクラスを作って登録してみました。あと、Sessionの後処理関連がどうなるのかよくわからなかったのですが・・・プロパティ設定の中に、「hibernate.transaction.flush_before_completion」、「hibernate.transaction.auto_close_session」という項目があったので、この2つを有効にしてみました。「コンテナ管理EntityManagerを作成する為の設定」が、調べても結局よくわからなかったのですが・・・とりあえずTransactionの設定には、コンテナ管理用のクラスを定義してみました。
以下が、hibernateのコンフィグファイルになります。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
<!--  
        <property name="hibernate.cglib.use_reflection_optimizer">true</property>
        <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="hibernate.connection.url">jdbc:hsqldb:hsql://localhost</property>
        <property name="hibernate.connection.username">sa</property>
-->
        <property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>
    	<property name="hibernate.use_sql_comments">true</property>
    	<property name="hibernate.connection.datasource">j2ee.dataSource</property>
    	<property name="hibernate.jndi.class">org.seasar.extension.j2ee.JndiContextFactory</property>
    	<property name="hibernate.transaction.factory_class">org.hibernate.transaction.CMTTransactionFactory</property>
    	<property name="hibernate.transaction.manager_lookup_class">test.hibernate.S2TransactionManagerLookup</property>
    	<property name="hibernate.transaction.flush_before_completion">true</property>
     	<property name="hibernate.transaction.auto_close_session">true</property>
    </session-factory>
</hibernate-configuration>

EntityBeanは、HSQLDBのデモデータに対して、Hibernate Toolsのリバースエンジニアリング機能を利用して自動作成しました。っていうか、これ凄く便利ですね・・・自動作成関連の規約のカスタマイズ方法がよくわからないのですが、とりあえずBeanを作成するだけなら、基本機能だけで充分でした。1対多、多対1等の関連設定も、DB側に外部キー制約があれば自動で作ってくれます。
後は・・・EntityBeanの登録を自動化したいですね。実際のEJB3では、parファイルという名前のjarにEntityをまとめて、その中に@Entity等が定義されたクラスに対して、自動登録を行ってるみたいです。Java EE サーバ環境ではその方がやりやすいんでしょうけど・・・ローカルで動かすのに、いちいちjarを作りたくはありません。そこで、Seasar2のComponentAutoRegisterを使って、指定したパッケージ内のEntityBeanを自動登録するクラスを作成してみました。

package test.autoregister;

import javax.persistence.Embeddable;
import javax.persistence.EmbeddableSuperclass;
import javax.persistence.Entity;

import org.hibernate.Interceptor;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.AnnotationConfiguration;
import org.seasar.framework.container.annotation.tiger.Binding;
import org.seasar.framework.container.annotation.tiger.BindingType;
import org.seasar.framework.container.autoregister.ComponentAutoRegister;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.ResourceUtil;


public class EntityComponentAutoRegister extends
		ComponentAutoRegister {

	private static final String DEFAULT_CONFIG_PATH = "hibernate.cfg.xml";

	private String configPath_ = DEFAULT_CONFIG_PATH;
	
	private Interceptor interceptor_;
	
	private AnnotationConfiguration configuration;
	
	private SessionFactory sessionFactory;
	
	public synchronized SessionFactory getSessionFactory() {
		if (sessionFactory == null) {
			registerAll();
		}
		return sessionFactory;
	}

	public void setConfigPath(String configPath) {
		configPath_ = configPath;
	}
	
	@Binding(bindingType = BindingType.MAY)
	public void setInterceptor(Interceptor interceptor) {
		interceptor_ = interceptor;
	}
	
	@Override
	public void registerAll() {
		configuration = new AnnotationConfiguration();
		configuration.configure(ResourceUtil.getResource(configPath_));

		super.registerAll();
		
		if (interceptor_ != null) {
			configuration.setInterceptor(interceptor_);
		}
		
		sessionFactory = configuration.buildSessionFactory();
	}

	@Override
	protected void register(String packageName, String shortClassName) {
		final String className = ClassUtil.concatName(packageName, shortClassName);
		Class<?> clazz = ClassUtil.forName(className);
		if (clazz.isAnnotationPresent(Entity.class)
			|| clazz.isAnnotationPresent(EmbeddableSuperclass.class)
			|| clazz.isAnnotationPresent(Embeddable.class)) {
			configuration.addAnnotatedClass(clazz);
		}
	}




		}

使ってていつも思うのですが、ComponentAutoRegisterクラスは本当に便利ですね。クラスパスさえ通っていれば、ファイルだろうがJarだろうがどこでも検索しに行くことが出来ます。EJB3もこれを使えばいいのに(笑)・・・さて、このクラスからSessionFactoryを受け取って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="org.hibernate.ejb.CurrentEntityManagerImpl">
		<arg>entityAutoRegister.sessionFactory</arg>
	</component>
	
	<component name="entityAutoRegister" class="test.autoregister.EntityComponentAutoRegister">
		<initMethod>#self.addReferenceClass(@test.autoregister.EntityComponentAutoRegister@class)</initMethod>
		<initMethod name="addClassPattern">
    	    <arg>"test"</arg>
        	<arg>".*"</arg>
	    </initMethod>
	</component>
</components>

これでDIすることが出来ます。DataSourceとTransactionManagerはJNDIで取得し、出来上がったEntityManagerはDIでユーザが利用出来る・・・つまり、Java EEサーバと同じEntityBeanの環境が、DIコンテナとHibernate EntityManagerの設定だけで構成出来るってことですね。
とりあえず、簡単なEJB-QL文を発行してみました。
(ちなみに、hibernate.cfx.xmlのsession-factoryタグのname属性をつけると、実行時にエラーになります。どうやらHibernateはこのname属性をつけると、InitialContextに対してrebind処理を行うらしく、そこでJndiContextが未対応例外を返してました。多分、JndiContextを継承して、rebind等の処理で何も行わない子クラスを作れば、うまく動きそうな気がします)。

List<Invoice> list = (List<Invoice>) em.createQuery(
			"SELECT iv FROM Invoice iv INNER JOIN FETCH iv.customer INNER JOIN FETCH iv.items WHERE iv.total >= :total")
			.setParameter("total", new BigDecimal("3000"))
			.getResultList();
		for (Invoice iv : list) {
			System.out.println("INVOICE_ID:" + iv.getId());
			System.out.println("FIRSTNAME:" + iv.getCustomer().getFirstname());
			System.out.println("TOTAL:" + iv.getTotal());
			for (Item item : iv.getItems()) {
				System.out.print("PRODUCT_NAME:" + item.getProduct().getName());
				System.out.println("\tPRODUCT_PRICE:" + item.getProduct().getPrice());
			}
}
DEBUG 2005-12-16 03:00:39,671 [main] BEGIN test.service.TestServiceImpl#execute()
DEBUG 2005-12-16 03:00:39,671 [main] トランザクションを開始しました
DEBUG 2005-12-16 03:00:40,218 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* SELECT
        iv 
    FROM
        Invoice iv 
    INNER JOIN
        FETCH iv.customer 
    INNER JOIN
        FETCH iv.items 
    WHERE
        iv.total >= :total */ select
            invoice0_.id as id1_0_,
            customer1_.id as id0_1_,
            items2_.INVOICEID as INVOICEID2_2_,
            items2_.ITEM as ITEM2_2_,
            invoice0_.CUSTOMERID as CUSTOMERID1_0_,
            invoice0_.total as total1_0_,
            customer1_.firstname as firstname0_1_,
            customer1_.lastname as lastname0_1_,
            customer1_.street as street0_1_,
            customer1_.city as city0_1_,
            items2_.PRODUCTID as PRODUCTID2_2_,
            items2_.quantity as quantity2_2_,
            items2_.cost as cost2_2_,
            items2_.INVOICEID as INVOICEID0__,
            items2_.ITEM as ITEM0__ 
        from
            Invoice invoice0_ 
        inner join
            Customer customer1_ 
                on invoice0_.CUSTOMERID=customer1_.id 
        inner join
            Item items2_ 
                on invoice0_.id=items2_.INVOICEID 
        where
            invoice0_.total>=?
DEBUG 2005-12-16 03:00:40,765 [main] 論理的なコネクションを閉じました
DEBUG 2005-12-16 03:00:40,781 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Product */ select
        product0_.id as id3_0_,
        product0_.price as price3_0_,
        product0_.name as name3_0_ 
    from
        Product product0_ 
    where
        product0_.id=?
DEBUG 2005-12-16 03:00:40,781 [main] 論理的なコネクションを閉じました
DEBUG 2005-12-16 03:00:40,796 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Product */ select
        product0_.id as id3_0_,
        product0_.price as price3_0_,
        product0_.name as name3_0_ 
    from
        Product product0_ 
    where
        product0_.id=?
DEBUG 2005-12-16 03:00:40,828 [main] 論理的なコネクションを閉じました
(中略)
DEBUG 2005-12-16 03:00:41,093 [main] 論理的なコネクションを閉じました
(表示略)
DEBUG 2005-12-16 03:00:41,843 [main] トランザクションをコミットしました
DEBUG 2005-12-16 03:00:41,843 [main] END test.service.TestServiceImpl#execute() : null

ふむ・・・とりあえず上手く動いたみたい・・・EJB-QLで結合したテーブルのデータは一気に取得され、EntityBeanに関連定義だけしているテーブルはLAZYロードされてます。
ただ・・・SQL文が発行される度に論理コネクションが都度クローズされてますね。いや、いいんだけど・・・コネクションを渡してSessionを作成するときと、DataSourceと連携させるときで、クローズのタイミングが異なるのかな?
(追記)(よく考えたら、コネクションでSession作ってるときは、そのコネクションを閉じられる訳ないじゃないか(汗)・・・)
あと、FLUSHやSessionのクローズがキチンとされてるかどうかの確認も必要そう・・・まぁ、ただ、Hibernate EntityManagerが、cfgファイル設定の定義だけで、普通に動かせるってことだけは確認出来ました。今後は、この環境でEJB3を勉強していこうかなと思ってます。来年になったらKuinaHibernateバージョンが出ると聞いてるので、それが出るまでの一時的な環境になるかもしれませんが(汗)・・・まぁHibernate独自のアノテーションとかもあるし、これを機会に少しでも勉強できればいいかな・・・
うーむ・・・ソースを引き続き読んでるんですけど・・・HibernateJTA関連機能はよく分からないところが多い。CMTTransactionがどんな動きをしてるのかもイマイチよくわかってないし・・・まぁ、ボチボチやっていきますか・・・