JPAのSQL機能って使いにくい?

会社で開発担当の皆さんに、簡単にJPAの機能を紹介してみたのですが、「便利になった」と思われるかと思ったら、まず第一に「EJB-QLが使えるか不安」という感想が返ってきました。まぁ自分のプレゼンが拙かったのもあるんでしょうが・・・少し意識のギャップがあったのではないかと実感。自分がHibernate EntityManagerの勉強をしていくうちに、いつのまにか「手段が目的」になってしまっていたのかもしれません。最初にHibernate EntityManagerの調査をはじめたときとは色々と仕事の条件が変わってきた・・・というのもありますし、ここで今一度ツールの比較を行う必要が出てきました。
そういえば、自分も元々はEJB-QL(HQL)のような擬似SQL言語には否定的な考えを持っていました。肯定的になったのは、実際に使ってみたら、SQLさえ知っていれば簡単に覚えられる内容だったのと、Hibernateの開発者自身が、「SQLを隠蔽したいのではなく、SQLをよく知った人間がより使いやすくなる為のもの」としてHibernateを位置づけているのを知ったからでした。なるほど、SQL作成ツールとしてHibernateを見てみれば、たしかに印象は随分変わってきます。
・・・が、その一方で、やはり純粋なSQLで書きたいという要求があるのは当然だと思いました。EJB-QLがいくらJava EEの標準になったとはいえ、SQLの世界で見れば一つのプログラム言語でしか使えない方言に過ぎず、それに比べてSQLはリレーショナルデータベースの世界の共通言語であり、かつ標準化も進んでいますし。
・・・というわけで、SQLをベースにしたO/Rマッピングも並行して調査候補に入れようと思ってます。今だとやはりS2Daoでしょうか。が、その前に、JPASQL機能を確認しておきたいと思いました。JPAでは一応SQLも使うことが出来ます。ざっと見でちょっと使い勝手が悪そうだったのですが、色々と使ってみれば、もしかしたら便利な利用方法があるかもしれませんし・・・
とりあえず、簡単にテスト。まずはテーブル。

CREATE TABLE CUSTOMER(
ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY,
NAME VARCHAR,
VERSION TIMESTAMP
)

CREATE TABLE ORDERS(
ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY,
NAME VARCHAR,
CUSTOMER_ID INTEGER,
VERSION TIMESTAMP
)

そしてEntity

package test;

import java.sql.Timestamp;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Version;

@Entity
public class Customer {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;
	
	@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
	private Set<Order> orders;
		
	@Version
	private Timestamp version;

	public Long getId() {
		return id;
	}

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

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Set<Order> getOrders() {
		return orders;
	}

	public void setOrders(Set<Order> orders) {
		this.orders = orders;
	}

	public Timestamp getVersion() {
		return version;
	}

	public void setVersion(Timestamp version) {
		this.version = version;
	}

}
package test;

import java.sql.Timestamp;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Version;

@Entity
@Table(name = "ORDERS")
public class Order {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;
	
	@ManyToOne
	private Customer customer;
	
	@Version
	private Timestamp version;

	public Customer getCustomer() {
		return customer;
	}

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

	public Long getId() {
		return id;
	}

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

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Timestamp getVersion() {
		return version;
	}

	public void setVersion(Timestamp version) {
		this.version = version;
	}

}

そして実行クラス。いつものように、メソッドにログとトランザクションアスペクトをかけてます。

package test;

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

import javax.persistence.EntityManager;

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

public class TestServiceImpl implements TestService {
	
	public static void main(String[] args) {

		SingletonS2ContainerFactory.init();
		S2Container container = SingletonS2ContainerFactory.getContainer();
		TestService service = (TestService) container.getComponent(TestService.class);
		service.execute2(service.execute1());

	}

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

	public Object execute1() {
		Customer customer = new Customer();
		customer.setName("CUSTOMER1");
		
		Set<Order> orders = new HashSet<Order>();
		
		Order order1 = new Order();
		order1.setName("ORDER1");
		order1.setCustomer(customer);
		orders.add(order1);
		
		Order order2 = new Order();
		order2.setName("ORDER2");
		order2.setCustomer(customer);
		orders.add(order2);
		
		customer.setOrders(orders);
		
		em.persist(customer);
		
		return customer.getId();

	}

	public void execute2(Object id) {

		Customer customer = (Customer) em.createNativeQuery(
			"SELECT"
				+ " c.ID,"
				+ " c.NAME,"
				+ " c.VERSION"
			+ " FROM"
				+ " CUSTOMER c"
			+ " WHERE"
				+ " c.ID = ?",
			Customer.class)
			.setParameter(1, id)
			.getSingleResult();
		
		System.out.println("CUSTOMER ID     :" + customer.getId());
		System.out.println("CUSTOMER NAME   :" + customer.getName());
		System.out.println("CUSTOMER VERSION:" + customer.getVersion());
		for (Order order : customer.getOrders()) {
			System.out.println("ORDER ID:" + order.getId());
			System.out.println("ORDER ID:" + order.getName());
			System.out.println("ORDER ID:" + order.getVersion());
		}
	}

}

この実行結果は

DEBUG 2006-01-22 07:58:53,453 [main] BEGIN test.TestServiceImpl#execute1()
DEBUG 2006-01-22 07:58:53,468 [main] トランザクションを開始しました
DEBUG 2006-01-22 07:58:53,687 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.Customer
        */ insert 
        into
            Customer
            (name, version, id) 
        values
            (?, ?, null)
DEBUG 2006-01-22 07:58:53,718 [main] 論理的なコネクションを閉じました
DEBUG 2006-01-22 07:58:53,718 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-01-22 07:58:53,718 [main] 論理的なコネクションを閉じました
DEBUG 2006-01-22 07:58:53,718 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.Order
        */ insert 
        into
            ORDERS
            (name, customer_id, version, id) 
        values
            (?, ?, ?, null)
DEBUG 2006-01-22 07:58:53,734 [main] 論理的なコネクションを閉じました
DEBUG 2006-01-22 07:58:53,734 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-01-22 07:58:53,734 [main] 論理的なコネクションを閉じました
DEBUG 2006-01-22 07:58:53,734 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* insert test.Order
        */ insert 
        into
            ORDERS
            (name, customer_id, version, id) 
        values
            (?, ?, ?, null)
DEBUG 2006-01-22 07:58:53,734 [main] 論理的なコネクションを閉じました
DEBUG 2006-01-22 07:58:53,734 [main] 論理的なコネクションを取得しました
Hibernate: 
    call identity()
DEBUG 2006-01-22 07:58:53,750 [main] 論理的なコネクションを閉じました
DEBUG 2006-01-22 07:58:53,812 [main] トランザクションをコミットしました
DEBUG 2006-01-22 07:58:53,812 [main] END test.TestServiceImpl#execute1() : 1
DEBUG 2006-01-22 07:58:53,812 [main] BEGIN test.TestServiceImpl#execute2(1)
DEBUG 2006-01-22 07:58:53,812 [main] トランザクションを開始しました
DEBUG 2006-01-22 07:58:53,906 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* dynamic native SQL query */ SELECT
        c.ID,
        c.NAME,
        c.VERSION 
    FROM
        CUSTOMER c 
    WHERE
        c.ID = ?
DEBUG 2006-01-22 07:58:54,125 [main] 論理的なコネクションを閉じました
CUSTOMER ID     :1
CUSTOMER NAME   :CUSTOMER1
CUSTOMER VERSION:2006-01-22 07:58:53.656
DEBUG 2006-01-22 07:58:54,140 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* load one-to-many test.Customer.orders */ select
        orders0_.customer_id as customer4_1_,
        orders0_.id as id1_,
        orders0_.id as id2_0_,
        orders0_.name as name2_0_,
        orders0_.customer_id as customer4_2_0_,
        orders0_.version as version2_0_ 
    from
        ORDERS orders0_ 
    where
        orders0_.customer_id=?
DEBUG 2006-01-22 07:58:54,171 [main] 論理的なコネクションを閉じました
ORDER ID:2
ORDER ID:ORDER2
ORDER ID:2006-01-22 07:58:53.734
ORDER ID:1
ORDER ID:ORDER1
ORDER ID:2006-01-22 07:58:53.718
DEBUG 2006-01-22 07:58:54,171 [main] トランザクションをコミットしました
DEBUG 2006-01-22 07:58:54,171 [main] END test.TestServiceImpl#execute2(1) : null

ここまでは別に普通でした。クラス単位で取得するなら、SQLでもさほど変わりませんね。しかし、ここでOrderのSetの値を同時に取得しようと思ったら・・・

@Entity
@SqlResultSetMapping(name = "C_AND_O", entities = {
	@EntityResult(name = "test.Customer"),
	@EntityResult(name = "test.Order", fields = {
		@FieldResult(name = "id", column = "ORDER_ID"),
		@FieldResult(name = "name", column = "ORDER_NAME"),
		@FieldResult(name = "version", column = "ORDER_VERSION")})
})
public class Customer {
	public void execute2(Object id) {

		List<Object[]> list = (List<Object[]>) em.createNativeQuery(
			"SELECT"
				+ " c.ID,"
				+ " c.NAME,"
				+ " c.VERSION,"
				+ " o.ID AS ORDER_ID,"
				+ " o.NAME AS ORDER_NAME,"
				+ " o.CUSTOMER_ID,"
				+ " o.VERSION AS ORDER_VERSION"
			+ " FROM"
				+ " CUSTOMER c"
			+ " LEFT OUTER JOIN"
				+ " ORDERS o"
			+ " ON"
				+ " c.ID = o.CUSTOMER_ID"
			+ " WHERE"
				+ " c.ID = ?",
			"C_AND_O")
			.setParameter(1, id)
			.getResultList();
		
		
		for (Object[] objs : list) {
			Customer customer = (Customer) objs[0];
			System.out.println("CUSTOMER ID     :" + customer.getId());
			System.out.println("CUSTOMER NAME   :" + customer.getName());
			System.out.println("CUSTOMER VERSION:" + customer.getVersion());
			Order order = (Order) objs[1];
			System.out.println("ORDER ID:" + order.getId());
			System.out.println("ORDER ID:" + order.getName());
			System.out.println("ORDER ID:" + order.getVersion());
			
		}
	}
DEBUG 2006-01-22 08:20:14,765 [main] BEGIN test.TestServiceImpl#execute2(3)
DEBUG 2006-01-22 08:20:14,765 [main] トランザクションを開始しました
DEBUG 2006-01-22 08:20:14,843 [main] 論理的なコネクションを取得しました
Hibernate: 
    /* dynamic native SQL query */ SELECT
        c.ID,
        c.NAME,
        c.VERSION,
        o.ID AS ORDER_ID,
        o.NAME AS ORDER_NAME,
        o.CUSTOMER_ID,
        o.VERSION AS ORDER_VERSION 
    FROM
        CUSTOMER c 
    LEFT OUTER JOIN
        ORDERS o 
            ON c.ID = o.CUSTOMER_ID 
    WHERE
        c.ID = ?
DEBUG 2006-01-22 08:20:15,093 [main] 論理的なコネクションを閉じました
CUSTOMER ID     :3
CUSTOMER NAME   :CUSTOMER1
CUSTOMER VERSION:2006-01-22 08:20:14.609
ORDER ID:5
ORDER ID:ORDER2
ORDER ID:2006-01-22 08:20:14.671
CUSTOMER ID     :3
CUSTOMER NAME   :CUSTOMER1
CUSTOMER VERSION:2006-01-22 08:20:14.609
ORDER ID:6
ORDER ID:ORDER1
ORDER ID:2006-01-22 08:20:14.687
DEBUG 2006-01-22 08:20:15,109 [main] トランザクションをコミットしました
DEBUG 2006-01-22 08:20:15,109 [main] END test.TestServiceImpl#execute2(3) : null

うーん・・・ちょっとこれじゃなぁ・・・
まず、@SqlResultSetMappingを書くのがめんどくさすぎる。EJB-QLだったら必要ない部分ですし。それに、今のHibernate EntityManagerの実装方法だと、このマッピングは@Entityをつけたクラスにつけないといけません。情報的にはDao等のSQLを記述する部分にこそ定義したい情報なんですが・・・EJB3コンテナだと、SessionBeanのメソッドに定義できたりするのかな?・・・でもそれってJPAの機能がEJB3に依存するって事だから無理な気がします。それに@SqlResultSetMappingの複数形が存在しないので、クラス、メソッドに一つずつしか登録できません。これって現実的に使い物にならない気がするのですが・・・
(追記:@NamedNativeQueriesを使えば、SQLとセットにして複数のマッピング情報を記述することが出来ますね。うーん・・・でも、そこまで書いたら最早アノテーションに書くべき情報じゃない気が・・・見た目も保守性も悪化しそうな気がします)
(さらに追記:@NamedNativeQueryのresultSetMapping属性はStringなので、この中に@SqlResultSetMappingの定義を直接書くことは出来ませんでした。ということは、やはり@SqlResultSetMappingはクラス・メソッドに一つしか定義できないということか・・・あぁめんどくさい。なんでこう面倒な定義になってるんだろう。もしかして、この機能自体あまり使って欲しくないのでしょうか(苦笑))
(さらにさらに追記:余談ですが、Hibernate EntityManagerについてくるJPAのクラスの@SqlResultSetMappingは、@Target({ElementType.PACKAGE, ElementType.TYPE})となってました。これって更新されてませんね。現時点では、全てのクラスがPFD対応になってるわけではないみたいです。PFDの定義を確認しながら使わないといけなさそうですね)
あと、EJB-QLのFETCH JOIN相当のことをやろうとしたときに、ひとつの永続化クラスの中に関連情報を入れた状態では取得することが出来ませんでした。結局、取得したいEntityをマッピング情報に書いて、Object配列として一行ずつ取得することになるみたいなんですが・・・これじゃあまりにも不便です。
EntityManagerのcreateNativeQueryって、そもそも何を目的に定義されたんだろう? データベース固有の記述表現をサポートするため? でも、そこまでして書きたいSQLって、やはり複雑なSELECT文のときのような気がするのですが・・・今の機能だと、単純に1つの永続化クラスの内容を取得するとき以外には、あまり有効には使えない気がします。
EJB-QLを使えば、JPAは検索機能についてもフルに実力を発揮出来ると思うのですが、SQLを使う場合は魅力半減してしまいますね・・・SQLを記述したいときには、JPA以外の選択肢がやはり必要ってことでしょうか。結局、Hibernateを選択する時の問題点と同じってことか・・・
自分の場合は、EJB-QL+JPA利用方法を教えるコストと、SQLベースのO/Rマッピングツールを教えるコストと、あとそれぞれのツールの機能との天秤になりそうです。JPAの機能自体は非常に豊富で魅力的なのですが、SQLを直接扱う機能がやはり最大の弱点ということになりそうですね。うーむ、JPAがすごく広まって、EJB-QLが当たり前のように使われるようになれば状況は変わると思うのですが、あくまで将来の予想ですし・・・
今回のJPA仕様が定義されるとき、あまりにもSQLに寄りすぎではないかとの批判が出たと聞いてますが、そこら辺を考慮して、SQLの直接サポート機能が中途半端なものになってしまったのかもしれませんね。個人的には、やはりSQLのサポート機能も充実させてもらいたいです。逆に言えば、これからのJPA実装は、SQLサポート機能を充実させれば、HibernateTopLinkとの差別化になるのかもしれません。