Hibernate Annotations

サーバサイドJava開発に関して、最近EJB3が話題になることが多いです。ですが個人的には、SessionBeanなどの「分散環境」としてのEJBにはもう興味が湧かないし、今更使おうとも思いません。・・・ですが、今後のO/Rマッピング標準になる可能性が高いjavax.persistenceパッケージには非常に興味があります。現在ベータ版が出ているHibernate Annotationsがこの機能を実装しているということなので、ちょっと使ってみました。
Hibernateのホームページから、Hibernate3.1とHibernate Annotationsをダウンロード。必須ライブラリをクラスパスに登録して準備完了。今回は、HSQLDBに標準でついてくるテストデータを利用して、EntityBeanを作ってみました。
・・・あ、ちなみにEJB3については雑誌でチョロっと読んだくらいの知識しかありません。まぁ、自分くらいのレベルの人間が軽く作れるようじゃないと「EoD」とは呼べないかな・・・ということで(笑)

Customerクラス

CUSTOMERテーブルのデータを格納するクラス

package test;

import java.io.Serializable;
import java.util.Set;

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

@Entity
public class Customer implements Serializable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 7521312932563435856L;

	private int id;
	
	private String firstName;
	
	private String lastName;
	
	private String street;
	
	private String city;
	
	private Set<Invoice> invoiceList;

	@OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
	public Set<Invoice> getInvoiceList() {
		return invoiceList;
	}

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

	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 int getId() {
		return id;
	}

	public void setId(int 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;
	}

}

EntityBeanであることを表す@Entity、キー項目につける@Id、1対多の結合を表す@OneToMany。クラス名やフィールド名を、利用するDBのテーブル名やカラム名に合わせれば、@Tableや@Column等の定義は必要ありません。殆ど最小限のアノテーションだけでEntityBeanを作成することが出来ます。

Invoiceクラス

package test;

import java.io.Serializable;
import java.math.BigDecimal;

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

@Entity
public class Invoice implements Serializable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = -5229848113066286613L;

	private int id;
	
	private Customer customer;
	
	private BigDecimal total;

	
	@ManyToOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "customerid")
	public Customer getCustomer() {
		return customer;
	}

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

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

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

	public BigDecimal getTotal() {
		return total;
	}

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

}

こちらは多対1の関係なので@MenyToOneアノテーションを定義。fetch属性を書かないと、デフォルトでLAZYが指定されるみたいで、この場合HibernateのLazy Load機能が働いて、必要になったときにだけDBにアクセスしに行くようになります。
また、@JoinColumnアノテーションは、結合条件となるテーブルのカラム名を指定します。


さて、早速実行してみようと思うのですが・・・折角ですから最近勉強しているSeasar2で動かしてみたいと思います。当然ここではS2Hibernateを使ってみたいです。ただし、Hibernate Annotationsはまだベータ版ですから、当然S2Hibernateは対応していません。自分でHibernate初期化の部分を調査して修正する必要があります。
通常のHibernateHibernate Annotationsの違いは、初期化するときにAnnotationConfigurationというクラスを使うところみたいです。S2Hibernateでこれを行っているのは、S2SessionFactoryImplクラスでした。このクラスはfinal指定されているので、ソース内容を参考にして、新たにFactoryクラスを作成することになります。作成したクラスをs2hibernate.diconファイルに登録して、S2Hibernate側の準備は終了。
では早速実行クラスを作ってみます。

CustomerDaoインターフェイス

package test;

public interface CustomerDao {
	Customer getCustomer(int id);
}

CustomerDaoImplクラス

package test;

import org.seasar.hibernate3.S2SessionFactory;

public class CustomerDaoImpl implements CustomerDao {

	private S2SessionFactory sessionFactory;

	public CustomerDaoImpl(S2SessionFactory factory) {
		sessionFactory = factory;
	}
	public Customer getCustomer(int id) {
		return (Customer) sessionFactory.getSession().get(Customer.class, id);
	}

}

CustomerServiceインターフェイス

package test;

public interface CustomerService {

	Customer getCustomer(int id);
}

CustomerServiceImplクラス

package test;

public class CustomerServiceImpl implements CustomerService {
	
	private CustomerDao dao;

	public CustomerServiceImpl(CustomerDao dao) {
		this.dao = dao;
	}

	public Customer getCustomer(int id) {
		return dao.getCustomer(id);
	}

}

app.dicon

<?xml version="1.0" encoding="Shift_JIS"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container//EN"
"http://www.seasar.org/dtd/components.dtd">
<components>
    <include path="s2hibernate3.dicon"/>

	<component class="test.CustomerDaoImpl"/>
	<component class="test.CustomerServiceImpl">
		<aspect>j2ee.requiredTx</aspect>
	</component>
</components>

hibernate3.cfg.xml

<?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>
    
    <mapping package="test"/>
    <mapping class="test.Customer"/>
    <mapping class="test.Invoice"/>

  </session-factory>
</hibernate-configuration>

Clientクラス

package test.client;

import java.util.Set;

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

import test.Customer;
import test.CustomerService;
import test.Invoice;

public class Client {

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		SingletonS2ContainerFactory.init();
		S2Container container = SingletonS2ContainerFactory.getContainer();

		CustomerService service = (CustomerService) container
				.getComponent(CustomerService.class);
		Customer customer = service.getCustomer(4);
		System.out.println("ID:" + customer.getId());
		System.out.println("FIRSTNAME:" + customer.getFirstName());
		System.out.println("LASTNAME:" + customer.getLastName());
		System.out.println("CITY:" + customer.getCity());
		System.out.println("STREET:" + customer.getStreet());
		Set<Invoice> set = customer.getInvoiceList();
		for (Invoice invoice : set) {
			System.out.println("\tINVOICEID:" + invoice.getId());
			System.out.println("\tTOTAL:" + invoice.getTotal());
		}

	}
}

実行結果

WARN  2005-09-19 20:34:48,609 [main] test.S2AnnotationSessionFactoryImplのプロパティ(interceptor)が見つからないので設定をスキップします
DEBUG 2005-09-19 20:34:49,015 [main] トランザクションを開始しました
log4j:WARN No appenders could be found for logger (org.hibernate.cfg.Environment).
log4j:WARN Please initialize the log4j system properly.
DEBUG 2005-09-19 20:34:51,093 [main] 物理的なコネクションを取得しました
DEBUG 2005-09-19 20:34:51,250 [main] 論理的なコネクションを取得しました
Hibernate: select customer0_.id as id0_1_, customer0_.firstName as firstName0_1_, customer0_.lastName as lastName0_1_, customer0_.city as city0_1_, customer0_.street as street0_1_, invoicelis1_.customerid as customerid3_, invoicelis1_.id as id3_, invoicelis1_.id as id1_0_, invoicelis1_.customerid as customerid1_0_, invoicelis1_.total as total1_0_ from Customer customer0_ left outer join Invoice invoicelis1_ on customer0_.id=invoicelis1_.customerid where customer0_.id=?
DEBUG 2005-09-19 20:34:51,531 [main] トランザクションをコミットしました
DEBUG 2005-09-19 20:34:51,546 [main] 論理的なコネクションを閉じました
ID:4
FIRSTNAME:Sylvia
LASTNAME:Ringer
CITY:Dallas
STREET:365 College Av.
	INVOICEID:18
	TOTAL:3772.80
	INVOICEID:35
	TOTAL:3102.60
	INVOICEID:40
	TOTAL:5288.40

実は、当初は@OneToManyのfetch属性を定義せずに実行したところエラーになってしまいました。POJOとして様々な用途に使うことを考えれば、fetchは標準でEAGERにしておいて欲しいなと思うのですが・・・これがEJB3の標準の動きになるんでしょうか?・・・
何はともあれ、かなり簡単にEJB3O/Rマッピング機能を利用できることがわかりました。Hibernateのhbmファイルが完全に無くなるっていうのはインパクト大きいですね。前回やった、S2AOPでアノテーションを利用したときも感じましたが、JDK5のアノテーションJavaの開発常識を覆す、大きな武器になりそうな気がします。