Hibernate EntityManagerその5 getReferenceとNamedQuery

一応環境構築関連がひと段落着いたので、EntityManagerの機能を一通り見ていこうと思ってます。Hibernate EntityManagerのリファレンスを見ながら、少しずつ進めてます。
そういや、EntityManagerをDIコンテナに登録する際、前回はDelegateInterceptorを使っていたのですが、やはり頻繁に使うコンポーネントなので、直接CurrentEntityManagerImplを生成して登録する方法に変更してみました。

<?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>emFactory.sessionFactory</arg>
	</component>
	
	<component name="emFactory" class="org.hibernate.ejb.HibernateEntityManagerFactory">
		<aspect>
			<component class="org.seasar.framework.aop.interceptors.DelegateInterceptor">
				<initMethod name="setTarget">
				<arg>@javax.persistence.Persistence@createEntityManagerFactory("test")</arg>
				</initMethod>
			</component>
		</aspect>
	</component>
	
</components>

これなら、一応EntityManagerFactoryのインスタンスも使おうと思えばDIで取得することも出来ますし、HibernateEntityManagerFactoryのインターフェイスを使えばSessionFactoryを取り出すことも出来ます。しばらくは、この設定で使っていこうかな?
さて、早速EntityManagerの各インターフェイスを触ってみたのですが・・・前々から気になっていたのが、getReferenceメソッドでした。コメントを読んでてもいまいちピンと来なかったのですが・・・Hibernateのリファレンスにわかりやすい例が載ってました。Entityを新規登録する際、関連するEntityをsetしたいが、その内容は別に取得する必要は無い場合、このgetReferenceを使ってプロキシーオブジェクトを取得し、登録対象の新規Entityにセットする。そうすると、Entityがpersistされるときに、関連するIDがinsertされる・・・という仕組みらしいです。なるほど、「オブジェクトは必要なんだけど、その内容は要らない」ってときに使えるんですね。・・・ただ、業務によっては、存在チェックをした上で関連付けを行いたい場合もあるかも?
・・・てな場面を想定して、ひとつサンプルを作ってみました。Entityは、毎回使っているHSQLDBデモデータのEntityBean(無駄なアノテーションを削除したり、手書きでシーケンス定義を追加したりしてるので、自動作成した内容とはちょっと違ってます)。今回は、EntityにNamedQueryを登録して、それをEntityManagerから利用するという方法で試してみたいと思います。

package test.generate.entity;

// Generated 2005/12/16 23:23:38 by Hibernate Tools 3.1.0 beta1JBIDERC2

import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratorType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;

/**
 * Invoice generated by hbm2java
 */
@Entity
@SequenceGenerator(name = "SQ_INVOICE", sequenceName = "SQ_INVOICE")
public class Invoice implements java.io.Serializable {

	// Fields

	/**
	 * 
	 */
	private static final long serialVersionUID = 4576239092234641160L;

	private Integer id;

	private Customer customer;

	private BigDecimal total;

	private Set<Item> items = new HashSet<Item>();

	// Constructors

	/** default constructor */
	public Invoice() {
	}

	/** minimal constructor */
	public Invoice(Integer id) {
		this.id = id;
	}

	/** full constructor */
	public Invoice(Integer id, Customer customer, BigDecimal total,
			Set<Item> items) {
		this.id = id;
		this.customer = customer;
		this.total = total;
		this.items = items;
	}

	// Property accessors
	@Id(generate = GeneratorType.SEQUENCE, generator = "SQ_INVOICE")
	public Integer getId() {
		return this.id;
	}

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

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "CUSTOMERID")
	public Customer getCustomer() {
		return this.customer;
	}

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

	public BigDecimal getTotal() {
		return this.total;
	}

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

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "invoice")
	public Set<Item> getItems() {
		return this.items;
	}

	public void setItems(Set<Item> items) {
		this.items = items;
	}

}
package test.generate.entity;

// Generated 2005/12/16 23:23:38 by Hibernate Tools 3.1.0 beta1JBIDERC2

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;

/**
 * Customer generated by hbm2java
 */
@Entity
@NamedQueries({
	@NamedQuery(name = Customer.EJBQL_INVOICE_EAGER, queryString = "SELECT c FROM Customer c INNER JOIN FETCH c.invoices WHERE c.id = :id"),
	@NamedQuery(name = Customer.EJBQL_EXIST_CHECK, queryString = "SELECT c.id FROM Customer c WHERE c.id = :id")
})
public class Customer implements java.io.Serializable {
	
	public static final String EJBQL_INVOICE_EAGER = "Customer.EJBQL_INVOICE_EAGER";

	public static final String EJBQL_EXIST_CHECK = "Customer.EJBQL_EXIST_CHECK";
	/**
	 * 
	 */
	private static final long serialVersionUID = -2253260523575317090L;

	// Fields

	private Integer id;

	private String firstname;

	private String lastname;

	private String street;

	private String city;

	private Set<Invoice> invoices = new HashSet<Invoice>();

	// Constructors

	/** default constructor */
	public Customer() {
	}

	/** minimal constructor */
	public Customer(Integer id) {
		this.id = id;
	}

	/** full constructor */
	public Customer(Integer id, String firstname, String lastname,
			String street, String city, Set<Invoice> invoices) {
		this.id = id;
		this.firstname = firstname;
		this.lastname = lastname;
		this.street = street;
		this.city = city;
		this.invoices = invoices;
	}

	// Property accessors
	@Id
	public Integer getId() {
		return this.id;
	}

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

	public String getFirstname() {
		return this.firstname;
	}

	public void setFirstname(String firstname) {
		this.firstname = firstname;
	}

	public String getLastname() {
		return this.lastname;
	}

	public void setLastname(String lastname) {
		this.lastname = lastname;
	}

	public String getStreet() {
		return this.street;
	}

	public void setStreet(String street) {
		this.street = street;
	}

	public String getCity() {
		return this.city;
	}

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

	@OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
	public Set<Invoice> getInvoices() {
		return this.invoices;
	}

	public void setInvoices(Set<Invoice> invoices) {
		this.invoices = invoices;
	}

}

今回は、この2つのEntityBeanを使います。InvoiceとCustomerは多対1の関係にあります。Customerクラスに、「EJBQL_EXIST_CHECK」という名前で存在チェックのEJBQLを書いて登録しています。
・・・で、このEntityBeanを使って新規登録するサンプルが↓

package test.service;

import java.math.BigDecimal;

import javax.persistence.EntityManager;

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

import test.generate.entity.Customer;
import test.generate.entity.Invoice;

public class InsertServiceImpl implements InsertService {
	
	private EntityManager em;
	
	public static void main(String[] args) {
		SingletonS2ContainerFactory.init();
		S2Container container = SingletonS2ContainerFactory.getContainer();

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


	public void setEm(EntityManager em) {
		this.em = em;
	}



	public void execute() {
		
		Invoice iv = new Invoice();
		Integer customerId = (Integer) em.createNamedQuery(Customer.EJBQL_EXIST_CHECK)
			.setParameter("id", 4)
			.getSingleResult();
		if (customerId != null) {
			iv.setCustomer(em.getReference(Customer.class, customerId));
		}
		iv.setTotal(new BigDecimal("500"));
		em.persist(iv);
		em.flush();
		
		System.out.println(iv.getId());
		System.out.println(iv.getTotal());
		System.out.println(iv.getCustomer().getFirstname());
		System.out.println(iv.getCustomer().getLastname());
		System.out.println(iv.getCustomer().getCity());
		System.out.println(iv.getCustomer().getStreet());

	}

}

前回と同じく、ログとトランザクションAOPを適用してます。Cusotmerは、候補となるIDのデータがDB上に存在するときだけ、getReferenceでプロキシーを取得しsetしてます。setが済んだ後persistメソッドで登録し、flushを行い、そして内容を表示させています。
このクラスの実行結果は・・・

DEBUG 2005-12-18 23:19:44,304 [main] BEGIN test.service.InsertServiceImpl#execute()
DEBUG 2005-12-18 23:19:44,336 [main] トランザクションを開始しました
DEBUG 2005-12-18 23:19:44,679 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* named HQL query Customer.EJBQL_EXIST_CHECK */ select
        customer0_.id as col_0_0_ 
    from
        Customer customer0_ 
    where
        customer0_.id=?
DEBUG 2005-12-18 23:19:44,726 [main] 論理的なコネクションを閉じました
DEBUG 2005-12-18 23:19:44,836 [main] 論理的なコネクションを取得しました
Hibernate: 
    select
        next value for SQ_INVOICE 
    from
        dual_SQ_INVOICE
DEBUG 2005-12-18 23:19:44,836 [main] 論理的なコネクションを閉じました
DEBUG 2005-12-18 23:19:45,023 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.generate.entity.Invoice
        */ insert 
        into
            Invoice
            (CUSTOMERID, total, id) 
        values
            (?, ?, ?)
DEBUG 2005-12-18 23:19:45,039 [main] 論理的なコネクションを閉じました
61
500
DEBUG 2005-12-18 23:19:45,039 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Customer */ select
        customer0_.id as id0_0_,
        customer0_.firstname as firstname0_0_,
        customer0_.lastname as lastname0_0_,
        customer0_.city as city0_0_,
        customer0_.street as street0_0_ 
    from
        Customer customer0_ 
    where
        customer0_.id=?
DEBUG 2005-12-18 23:19:45,054 [main] 論理的なコネクションを閉じました
Sylvia
Ringer
Dallas
365 College Av.
DEBUG 2005-12-18 23:19:45,054 [main] トランザクションをコミットしました
DEBUG 2005-12-18 23:19:45,054 [main] END test.service.InsertServiceImpl#execute() : null

登録候補のCUSTOMERIDを検索し、存在したのでプロキシーを取得し(プロキシー取得時にSQLは発行されてません)、persist。その後、flushを実行したので、INVOICE登録用のSEQUENCEを発行してINSERT.。その後内容を表示させるときにCustomerにアクセスしたので、ここで初めてCustomerオブジェクトの内容を取得しに行ってます。存在チェックが必要無ければ、いきなりgetReferenceで関連付けを行ってしまえばOKってことですね。なるほど・・・極力、無駄なSQLは発行せずにすみそうです。
この例では、NamedQueryを使って存在チェック用のEJBQLを登録していました。NamedQueryはDao側ではなくてEntityの方に登録するので、最初はあまり使い勝手が良くないのではないかと思っていたのですが・・・今回のような存在チェックとか、テーブル定義に関連した定型的な作業をこうやって登録してやれば、Daoの記述が楽になるし、テーブルに関連した定義をEntityに集中させることも出来そうですね。意外と使い勝手の良いアノテーションなのかも・・・
ここで、もう一つ例を考えてみました。このEntityManagerはどうやら、取得したEntityがトランザクションの外・プレゼンテーションレイヤで使われることを想定して定義が行われているみたいです。トランザクションの外で変更を行ったEntityを、別のトランザクションの中でmergeを行い、変更を反映させる・・・このような一連の流れを想定しているみたい。ただ、ここで問題になるのが遅延ロード機能。自分もあまりHibernateのことが良くわかってなかったとき、無理矢理Entityの関連を全部EAGERにして対応したりしてましたが(汗)・・・普通に考えれば、デフォルトのオブジェクトの関連設定は、必ずペアで使うような関連でない限り、LAZYで設定しておく方がいいと思います。EntityBeanはデフォルトでOneToManyやManyToManyをLAZYモードで関連定義するみたいです。が、個人的にはManyToOneやOneToOneもデフォルトはLAZYでいいんじゃないかと思うんですよね。ただ、そうしてしまうと、プレゼンテーションレイヤで使わせたいというEntityManager側の意図に乗った場合、遅延ロード機能が使えないわけで・・・
そんなときには、EJBQLのFETCH JOIN機能を使うことになるわけなんですが・・・いちいち書くのがメンドくさい。しかし、実際書いてみれば、「INNER JOIN FETCH c.invoices」みたいなことを書くだけなので・・・これもあらかじめEntity側にNamedQueryとして登録しておけばいいんじゃないかと思いました。それで作ってみたのが2番目の例。

package test.service;

import javax.persistence.EntityManager;

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

import test.generate.entity.Customer;
import test.generate.entity.Invoice;

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

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

		SelectService service = (SelectService) container.getComponent(SelectService.class);

		Customer customer = service.getCustomerForPresentation(3);
		viewCustomer(customer);
		
		customer = service.getCustomer(3);
		viewCustomer(customer);
		
	}

	private static void viewCustomer(Customer customer) {
		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());
		for (Invoice invoice : customer.getInvoices()) {
			System.out.println("\tID:" + invoice.getId());
			System.out.println("\tTOTAL:" + invoice.getTotal());
		}
		System.out.println();
	}

	public Customer getCustomerForPresentation(Integer id) {
		return (Customer) em.createNamedQuery(Customer.EJBQL_INVOICE_EAGER)
		.setParameter("id", id)
		.getSingleResult();
	}
	
	public Customer getCustomer(Integer id) {
		return em.find(Customer.class, id);
	}
	

}

EJBQLでFETCH JOINで取得した後、findメソッドで同じ処理を行い、いずれもトランザクションの外で内容表示を行ってみました。結果は・・・

DEBUG 2005-12-18 23:38:51,070 [main] BEGIN test.service.SelectServiceImpl#getCustomerForPresentation(3)
DEBUG 2005-12-18 23:38:51,086 [main] トランザクションを開始しました
DEBUG 2005-12-18 23:38:51,211 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* named HQL query Customer.EJBQL_INVOICE_EAGER */ select
        customer0_.id as id0_0_,
        invoices1_.id as id1_1_,
        customer0_.firstname as firstname0_0_,
        customer0_.lastname as lastname0_0_,
        customer0_.city as city0_0_,
        customer0_.street as street0_0_,
        invoices1_.CUSTOMERID as CUSTOMERID1_1_,
        invoices1_.total as total1_1_,
        invoices1_.CUSTOMERID as CUSTOMERID0__,
        invoices1_.id as id0__ 
    from
        Customer customer0_ 
    inner join
        Invoice invoices1_ 
            on customer0_.id=invoices1_.CUSTOMERID 
    where
        customer0_.id=?
DEBUG 2005-12-18 23:38:51,273 [main] 論理的なコネクションを閉じました
DEBUG 2005-12-18 23:38:51,320 [main] トランザクションをコミットしました
DEBUG 2005-12-18 23:38:51,320 [main] END test.service.SelectServiceImpl#getCustomerForPresentation(3) : test.generate.entity.Customer@1980630
ID:3
FIRSTNAME:Michael
LASTNAME:Clancy
CITY:San Francisco
STREET:542 Upland Pl.
	ID:54
	TOTAL:500
	ID:50
	TOTAL:400
	ID:51
	TOTAL:500

DEBUG 2005-12-18 23:38:51,336 [main] BEGIN test.service.SelectServiceImpl#getCustomer(3)
DEBUG 2005-12-18 23:38:51,336 [main] トランザクションを開始しました
DEBUG 2005-12-18 23:38:51,336 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load test.generate.entity.Customer */ select
        customer0_.id as id0_0_,
        customer0_.firstname as firstname0_0_,
        customer0_.lastname as lastname0_0_,
        customer0_.city as city0_0_,
        customer0_.street as street0_0_ 
    from
        Customer customer0_ 
    where
        customer0_.id=?
DEBUG 2005-12-18 23:38:51,336 [main] 論理的なコネクションを閉じました
DEBUG 2005-12-18 23:38:51,351 [main] トランザクションをコミットしました
DEBUG 2005-12-18 23:38:51,351 [main] END test.service.SelectServiceImpl#getCustomer(3) : test.generate.entity.Customer@1a5db4b
ID:3
FIRSTNAME:Michael
LASTNAME:Clancy
CITY:San Francisco
STREET:542 Upland Pl.
Exception in thread "main" org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: test.generate.entity.Customer.invoices, 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.SelectServiceImpl.viewCustomer(SelectServiceImpl.java:38)
	at test.service.SelectServiceImpl.main(SelectServiceImpl.java:28)
2005-12-18 23:38:51,429 [main] ERROR org.hibernate.LazyInitializationException - failed to lazily initialize a collection of role: test.generate.entity.Customer.invoices, no session or session was closed
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: test.generate.entity.Customer.invoices, 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.SelectServiceImpl.viewCustomer(SelectServiceImpl.java:38)
	at test.service.SelectServiceImpl.main(SelectServiceImpl.java:28)

FETCH JOINで取得すれば問題なく表示され、findで取得した場合は遅延ロードでエラーが発生しました。予定通りですね。
ここで、Customer側に定義していたNamedQueryの内容は・・・

SELECT c FROM Customer c INNER JOIN FETCH c.invoices WHERE c.id = :id

この程度の内容だったら、FETCH JOINが必要になる度にEntityにNamedQueryを追加していけば、Dao側にはEJBQLを書く必要も無い気がします。プレゼンテーションレイヤでロードを行うような機能はやはり使いたくないので。EJB3ではこんな感じでEJBQLを登録しておいた上で、findメソッドと併用して使い分けていけば、そこそこ簡単に作っていけそうな気がしますね。これなら、merge機能もどんどん活用していけそうですし。
また、単純な定型的EJBQLは、Hibernate Tools等でEntityを自動作成するときに、ついでに作ってくれると嬉しいのですが(笑)存在チェックとか、FETCH JOINとか、単純な機能であれば作成テンプレート側で対応出来そうな気がしますし。