Hibernate EntityManager

「ローカル環境でもEntityBean利用環境をEJB3と同じ方式で提供」という謳い文句に惹かれて、ちょっと触ってみました。ですが・・・parファイルにクラス群をまとめる必要があるとか、ローカルではJDBCトランザクションしか使えないとか・・・ちょっと微妙なかんじ・・・

In a J2SE environment only application-managed entity managers are available. You can retrieve an entity manger using the EntityManagerFactory API. Only resource-local entity managers are available. In other words, JTA transactions and persistence context propagation are not supported in J2SE (you will have to propagate the persistence context yourself, e.g. using the thread local session pattern popular in the Hibernate community).

うーむ・・・これだったら、Hibernate Annotationsで普通に使ってた方がマシかも・・・
EntityManagerFactory関連を眺めていたのですが、やっぱり色々めんどくさそう。もう止めようかな・・・と思いながら、EntityManagerの実装クラスの方を調べてみたところ、実は「SessionFactoryをラップして使ってるだけ」というメソッドが殆どなことに気付きました。これだったら、SessionFactoryをS2SessionFactoryに変えて、トランザクションが絡む処理をちょっと変更すれば、簡単に動くかも?・・・というわけで、Factory関連は無視して、EntityManagerImplをいじってみることにしました。

package test.hibernate;

import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContextType;
import javax.persistence.TransactionRequiredException;
import javax.transaction.TransactionManager;

import org.hibernate.MappingException;
import org.hibernate.ObjectDeletedException;
import org.hibernate.Session;
import org.hibernate.StaleObjectStateException;
import org.hibernate.UnresolvableObjectException;
import org.hibernate.ejb.EntityManagerImpl;
import org.seasar.framework.util.TransactionManagerUtil;
import org.seasar.hibernate3.S2SessionFactory;

@SuppressWarnings("serial")
public class EntityManagerImplUpdate extends EntityManagerImpl {

	private S2SessionFactory sessionFactory;

	private TransactionManager transactionManager;

	public EntityManagerImplUpdate(S2SessionFactory sessionFactory,
			TransactionManager transactionManager) {
		super(null, PersistenceContextType.TRANSACTION);
		this.sessionFactory = sessionFactory;
		this.transactionManager = transactionManager;
	}

	@Override
	public Session getSession() {

		return sessionFactory.getSession();
	}

	private void checkTransactionActive() {
		if (TransactionManagerUtil.getTransaction(transactionManager) == null) {
			throw new TransactionRequiredException("no transaction is in progress");
		}
	}

	@Override
	public void persist(Object entity) {
		checkTransactionActive();
		try {
			getSession().persist(getEntityName(entity.getClass()), entity);
		} catch (MappingException e) {
			throw new IllegalArgumentException(e.getMessage());
		}
	}

	@Override
	public <A> A merge(A entity) {
		checkTransactionActive();
		try {
			return (A) getSession().merge(getEntityName(entity.getClass()), entity);
		} catch (StaleObjectStateException sse) {
			throw new IllegalArgumentException(sse);
		} catch (ObjectDeletedException sse) {
			throw new IllegalArgumentException(sse);
		} catch (MappingException e) {
			throw new IllegalArgumentException(e.getMessage(), e);
		}
	}

	@Override
	public void remove(Object entity) {
		checkTransactionActive();
		try {
			getSession().delete(entity);
		} catch (MappingException e) {
			throw new IllegalArgumentException(e.getMessage(), e);
		}
	}

	@Override
	public void refresh(Object entity) {
		checkTransactionActive();
		try {
			getSession().refresh(entity);
		} catch (UnresolvableObjectException uoe) {
			throw new EntityNotFoundException(uoe);
		} catch (MappingException e) {
			throw new IllegalArgumentException(e.getMessage(), e);
		}
	}

}

親クラスで使っていたSessionFacotryの代わりにS2SessionFactoryを利用して、getSessionメソッドをオーバーライド。親クラスのcheckTransactionActiveメソッドは、SessionオブジェクトをSessionImplementorにキャストして、isTransactionInProgressを呼び出す・・・という、何か無理な作り方がされてました(汗)どうやら、実行する時にトランザクションが実行されているかをチェックする為に色々やってるみたいなんですが・・・TransactionManagerをEntityManagerのフィールドに持って直接チェックする方法に変えてみました。
次にhibernate.cfg.xmlへの登録ですが・・・前々から試したいと思っていた、ComponentAutoRegisterを利用して、@Entityをつけたクラスを自動的にHibernateに登録する方法を試してみました。

package test.autoregister;

import javax.persistence.Entity;

import org.hibernate.Interceptor;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Configuration;
import org.seasar.framework.container.autoregister.FileSystemComponentAutoRegister;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.ResourceUtil;


public class EntityComponentAutoRegister extends
		FileSystemComponentAutoRegister {

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

	private String configPath_ = DEFAULT_CONFIG_PATH;
	
	private Interceptor interceptor_;
	
	private AnnotationConfiguration configuration;
	
	public String getConfigPath() {
		return configPath_;
	}

	public void setConfigPath(String configPath) {
		configPath_ = configPath;
	}
	
	public void setInterceptor(Interceptor interceptor) {
		interceptor_ = interceptor;
	}
	
	public synchronized Configuration getConfiguration() {
		if (configuration == null) {
			registAll();
		}
		return configuration;
	}
	
	@Override
	public void registAll() {
		configuration = new AnnotationConfiguration();
		configuration.configure(ResourceUtil.getResource(configPath_));

		super.registAll();
		
		if (interceptor_ != null) {
			configuration.setInterceptor(interceptor_);
		}
	}

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




}

registメソッドで@Entityをチェックして、アノテーションがあればAnnotationConfigurationにクラスを追加します。・・・このやり方って、様々なフレームワークに使えるような気が・・・AutoRegister自体が、当初からそういう利用を狙って作られてるのかも?・・・StrutsやらJSFやら、色々なクラス登録に使われるようになれば、XML地獄なんて無縁の開発生活が来るのもそう遠くないかもですね。
話を戻しまして・・・↑で作ったConfigurationを利用して初期化をするように、S2SessionFactoryクラスをちょっと変更。元クラスがfinalクラスなので記述は省略します。
設定ファイルはこんなかんじになりました。

    <component class="test.hibernate.S2SessionFactoryForEntityManager" >
    	<property name="configuration">entityAutoRegister.configuration</property>
	</component>
	
	<component name="entityAutoRegister" class="test.autoregister.EntityComponentAutoRegister">
		<property name="configPath">"hibernate3.cfg.xml"</property>	   		  
		<initMethod name="addClassPattern">
    	    <arg>"test"</arg>
        	<arg>".*"</arg>
	    </initMethod>
    	<initMethod name="registAll"/>
	</component>
        
    <component class="test.hibernate.EntityManagerImplUpdate"/>
<?xml version="1.0"?>
<!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="dialect">org.hibernate.dialect.HSQLDialect</property>
    <property name="show_sql">true</property>
    
  </session-factory>
</hibernate-configuration>

hbm.xmlは必要無いので無し。cfg.xmlもこれだけだったら、Configuration作るときにdiconからパラメータ渡した方が早いかも?(汗)
で、次は@Entityを作成。以前Hibernate Annotationsを触ったときのように、HSQLDB付属のサンプルデータを使ってみました。

package test.entity;

import java.util.List;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class Customer {
	
	private Integer id;
	
	private String firstName;
	
	private String lastName;
	
	private String street;
	
	private String city;
	
	private List<Invoice> invoiceList;

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	@Id
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getStreet() {
		return street;
	}

	public void setStreet(String street) {
		this.street = street;
	}
	
	@OneToMany(mappedBy="customer")
	public List<Invoice> getInvoiceList() {
		return invoiceList;
	}

	public void setInvoiceList(List<Invoice> invoiceList) {
		this.invoiceList = invoiceList;
	}



}
package test.entity;

import java.math.BigDecimal;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

@Entity
public class Invoice {
	
	private Integer id;
	
	private Customer customer;
	
	private BigDecimal total;

	@ManyToOne
	@JoinColumn(name="CUSTOMERID")
	public Customer getCustomer() {
		return customer;
	}

	public void setCustomer(Customer customer) {
		this.customer = customer;
	}

	@Id
	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public BigDecimal getTotal() {
		return total;
	}

	public void setTotal(BigDecimal total) {
		this.total = total;
	}

}

これらの@Entityを扱うDao

package test.dao;


import java.util.List;

import javax.persistence.EntityManager;

import test.entity.Customer;

public class TestDaoImpl implements TestDao {
	
	private EntityManager entityManager;
	
	

	public TestDaoImpl(EntityManager entityManager) {
		this.entityManager = entityManager;
	}

	public Customer getCustomer(int id) {
		
		return entityManager.find(Customer.class, id);
		
	}

	public void insertCustomer(Customer customer) {
		Customer cm = (Customer) entityManager.createNativeQuery("SELECT * FROM CUSTOMER WHERE ID=(SELECT MAX(ID) FROM CUSTOMER)", Customer.class).getSingleResult();
		customer.setId(cm.getId() + 1);
		entityManager.persist(customer);
	}

	public List<Customer> getCustomerList() {
		return (List<Customer>) entityManager.createQuery("from test.entity.Customer").getResultList();
	}

}

@IdのGenerator設定関連がよく分からなかったので、ID作成部分は適当に作成。っていうか・・・EntityManagerでEntityを1個取得するときはGenericsが効くんですが、Query使ってListで取得するときは型指定出来ないじゃん・・・なんでこんな中途半端な作り方にしてるんだろ?・・・
このDaoを利用するクラス

package test.service;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.beanutils.PropertyUtils;

import test.dao.TestDao;
import test.entity.Customer;

public class TestServiceImpl implements TestService {
	
	private TestDao dao;
	public TestServiceImpl(TestDao dao) {
		this.dao = dao;
	}
	/* (非 Javadoc)
	 * @see test.service.TestService#insertCustomer(test.entity.Customer)
	 */
	public void insertCustomer(Customer customer) {
		dao.insertCustomer(customer);
	}

	public Customer getCustomer(int id) {
		Customer customer = dao.getCustomer(id);
		Customer ret = new Customer();
		try {
			PropertyUtils.copyProperties(ret, customer);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}
		return ret;
	}
	
	public List<Customer> getCustomerList() {
		List<Customer> list = dao.getCustomerList();
		List<Customer> ret = new ArrayList<Customer>();
		try {
			for (Customer cm : list) {
				Customer customer = new Customer();
				PropertyUtils.copyProperties(customer, cm);
				ret.add(customer);
			}
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}
		return ret;
	}

}

・・・適当です(汗)
クライアントは色々形を変えて使ったので一部だけ記述

		SingletonS2ContainerFactory.init();
		S2Container container = SingletonS2ContainerFactory.getContainer();
		TestService service = (TestService) container.getComponent(TestService.class);
		
		
		Customer customer = service.getCustomer(32);
		System.out.println(customer.getId());
		System.out.println(customer.getLastName());
		System.out.println(customer.getFirstName());
		System.out.println(customer.getCity());
		System.out.println(customer.getStreet());
		System.out.println();
		for (Invoice invoice : customer.getInvoiceList()) {
			System.out.println(invoice.getId());
			System.out.println(invoice.getCustomer().getFirstName());
			System.out.println(invoice.getTotal());
			System.out.println();
		}

この結果が

DEBUG 2005-10-24 08:00:42,968 [main] トランザクションを開始しました
DEBUG 2005-10-24 08:00:44,734 [main] 物理的なコネクションを取得しました
DEBUG 2005-10-24 08:00:44,890 [main] 論理的なコネクションを取得しました
Hibernate: select customer0_.id as id0_0_, customer0_.lastName as lastName0_0_, customer0_.firstName as firstName0_0_, customer0_.city as city0_0_, customer0_.street as street0_0_ from Customer customer0_ where customer0_.id=?
Hibernate: select invoicelis0_.CUSTOMERID as CUSTOMERID1_, invoicelis0_.id as id1_, invoicelis0_.id as id1_0_, invoicelis0_.CUSTOMERID as CUSTOMERID1_0_, invoicelis0_.total as total1_0_ from Invoice invoicelis0_ where invoicelis0_.CUSTOMERID=?
DEBUG 2005-10-24 08:00:45,328 [main] トランザクションをコミットしました
DEBUG 2005-10-24 08:00:45,343 [main] 論理的なコネクションを閉じました
32
Ott
Michael
Boston
339 College Av.

44
Michael
3388.20

49
Michael
4944.30

次はListの表示例

		List<Customer> customerList = service.getCustomerList();
		for (Customer cm : customerList) {
			System.out.println(cm.getId());
			System.out.println(cm.getLastName());
			System.out.println(cm.getFirstName());
			System.out.println(cm.getCity());
			System.out.println(cm.getStreet());
			System.out.println();
			for (Invoice invoice : cm.getInvoiceList()) {
				System.out.println(invoice.getId());
				System.out.println(invoice.getCustomer().getFirstName());
				System.out.println(invoice.getTotal());
				System.out.println();
			}
			
		}

この結果が

DEBUG 2005-10-24 08:03:07,531 [main] トランザクションを開始しました
DEBUG 2005-10-24 08:03:09,312 [main] 物理的なコネクションを取得しました
DEBUG 2005-10-24 08:03:09,468 [main] 論理的なコネクションを取得しました
Hibernate: select customer0_.id as id0_, customer0_.lastName as lastName0_, customer0_.firstName as firstName0_, customer0_.city as city0_, customer0_.street as street0_ from Customer customer0_
Hibernate: select invoicelis0_.CUSTOMERID as CUSTOMERID1_, invoicelis0_.id as id1_, invoicelis0_.id as id1_0_, invoicelis0_.CUSTOMERID as CUSTOMERID1_0_, invoicelis0_.total as total1_0_ from Invoice invoicelis0_ where invoicelis0_.CUSTOMERID=?
Hibernate: select invoicelis0_.CUSTOMERID as CUSTOMERID1_, invoicelis0_.id as id1_, invoicelis0_.id as id1_0_, invoicelis0_.CUSTOMERID as CUSTOMERID1_0_, invoicelis0_.total as total1_0_ from Invoice invoicelis0_ where invoicelis0_.CUSTOMERID=?
Hibernate: select invoicelis0_.CUSTOMERID as CUSTOMERID1_, invoicelis0_.id as id1_, invoicelis0_.id as id1_0_, invoicelis0_.CUSTOMERID as CUSTOMERID1_0_, invoicelis0_.total as total1_0_ from Invoice invoicelis0_ where invoicelis0_.CUSTOMERID=?
Hibernate: select invoicelis0_.CUSTOMERID as CUSTOMERID1_, invoicelis0_.id as id1_, invoicelis0_.id as id1_0_, invoicelis0_.CUSTOMERID as CUSTOMERID1_0_, invoicelis0_.total as total1_0_ from Invoice invoicelis0_ where invoicelis0_.CUSTOMERID=?

(中略)

DEBUG 2005-10-24 08:03:11,000 [main] トランザクションをコミットしました
DEBUG 2005-10-24 08:03:11,000 [main] 論理的なコネクションを閉じました
0
Steel
Laura
Dallas
429 Seventh Av.

0
Laura
2607.60

1
King
Susanne
Olten
366 - 20th Ave.

2
Miller
Anne
Lyon
20 Upland Pl.

3
Clancy
Michael
San Francisco
542 Upland Pl.

4
Ringer
Sylvia
Dallas
365 College Av.

18
Sylvia
3772.80

35
Sylvia
3102.60

40
Sylvia
5288.40


(後略)

最後にINSERT

		Customer cm = new Customer();
		cm.setLastName("小泉");
		cm.setFirstName("純一郎");
		cm.setCity("東京");
		cm.setStreet("港区");
		service.insertCustomer(cm);
		
DEBUG 2005-10-23 23:20:50,436 [main] トランザクションを開始しました
DEBUG 2005-10-23 23:20:52,202 [main] 物理的なコネクションを取得しました
DEBUG 2005-10-23 23:20:52,374 [main] 論理的なコネクションを取得しました
Hibernate: SELECT * FROM CUSTOMER WHERE ID=(SELECT MAX(ID) FROM CUSTOMER)
Hibernate: insert into Customer (city, firstName, lastName, street, id) values (?, ?, ?, ?, ?)
DEBUG 2005-10-23 23:20:52,702 [main] トランザクションをコミットしました
DEBUG 2005-10-23 23:20:52,702 [main] 論理的なコネクションを閉じました

DB見たらちゃんと入ってました。まぁ、こんなもんか・・・っていうか、実験とかやる前に、@Entityについてもうちっと勉強しないと・・・(汗)


EJB3のEntityBean関連の機能を試してみたいけど、EJBサーバ環境を構築するのはちょっとメンドクサイってときは、こんな感じでローカルでEntityManagerが動く環境を作ってやれば、後は結構簡単に色々テスト出来るのではないかと思ってます。EJB3自体の公開スケジュールとかよく知らないのですが、そろそろ雑誌等での特集とか増えてくるかもしれないし。
あんまし機能を検証したわけじゃないけど、Query関連の機能はもうちょっと勉強してみたいと思いました。結局、Listの型指定が出来ないとなると、かなり中途半端なものになってしまう気がするのですが・・・ただ、HibernateTopLink等のメジャーなO/Rマッピングがどれもこの方法で使えるようになるのであれば、結果的にこれが標準の有力候補となるのかな?・・・
一方、@Entityアノテーションのチェックによって、Hibernateの定義ファイルがほとんど必要無くなるのは非常に魅力的ですね。一度出来ると知ったら、もうちまちまとxmlにクラス名書くなんて耐えられなくなるかもしれない(汗)そういや、本当はEntityManager用に定められた設定ファイルとかあるみたいなんですが・・・せっかく定義ファイルが要らなくなったんだから、今回は無視しました(汗)何でも標準に合わせりゃいいってもんでもないだろうし。
そういやEJB3って、この@Entity以外は、あとどんな機能があったんでしたっけ? @Statelessとかの設定が出来るぐらい? DI機能とかは「@Inject」アノテーションとか使って、Contextからのlookupを代用するとか聞いてるのですが・・・なんか、今更な機能だなという気がします(汗)@Entityさえ動けば、EJB3自体の登場を待つ必要性をあまり感じなくなってきました・・・