GlassFish V3 b67を使ってJava Persistence 2.0を試す
久々の日記です。
EclipseLinkのJPA2.0の実装具合を最近のGlassFish V3を動かしてチェックしていたのですが、b67になってようやく基本的な機能が実装されたようです。なので、簡単に動かしてみました。動作環境はGlassFishV3-b67、DBはMySQLです。
まずはテーブル定義。単純なオーダ受注用のテーブルです。
-- phpMyAdmin SQL Dump -- version 3.2.2 -- http://www.phpmyadmin.net -- -- ホスト: localhost -- 生成時間: 2009 年 10 月 13 日 11:54 -- サーバのバージョン: 5.0.86 -- PHP のバージョン: 5.3.0 SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; -- -- データベース: 'testdb' -- -- -------------------------------------------------------- -- -- テーブルの構造 'products' -- CREATE TABLE products ( id int(11) NOT NULL auto_increment COMMENT 'ID', `name` varchar(255) collate utf8_unicode_ci NOT NULL COMMENT '名前', price decimal(10,0) NOT NULL COMMENT '価格', created datetime NOT NULL COMMENT '登録日', updated datetime NOT NULL COMMENT '更新日', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='製品'; -- -- テーブルのデータをダンプしています 'products' -- INSERT INTO products (id, name, price, created, updated) VALUES(1, 'Zend Framework', '1200', '2009-10-09 15:28:35', '2009-10-09 15:28:35'); INSERT INTO products (id, name, price, created, updated) VALUES(2, 'CakePHP', '800', '2009-10-09 15:29:18', '2009-10-09 15:29:18'); INSERT INTO products (id, name, price, created, updated) VALUES(3, 'Symfony', '3000', '2009-10-09 15:29:18', '2009-10-09 15:29:18'); -- -------------------------------------------------------- -- -- テーブルの構造 'users' -- CREATE TABLE users ( id int(11) NOT NULL auto_increment COMMENT 'ID', `name` varchar(255) collate utf8_unicode_ci NOT NULL COMMENT '名前', sex tinyint(1) NOT NULL COMMENT '性別', address varchar(255) collate utf8_unicode_ci default NULL COMMENT '住所', created datetime NOT NULL COMMENT '登録日', updated datetime NOT NULL COMMENT '更新日', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='ユーザー'; -- -- テーブルのデータをダンプしています 'users' -- INSERT INTO users (id, name, sex, address, created, updated) VALUES(1, '吉田', 0, '東京都', '2009-10-09 00:00:00', '2009-10-09 00:00:00'); INSERT INTO users (id, name, sex, address, created, updated) VALUES(2, '鈴木一郎', 0, '東京都', '2009-10-09 06:27:52', '2009-10-09 06:27:52'); -- -------------------------------------------------------- -- -- テーブルの構造 'order_details' -- CREATE TABLE order_details ( id int(11) NOT NULL auto_increment COMMENT 'ID', pd_order_id int(11) NOT NULL COMMENT '注文', product_id int(11) NOT NULL COMMENT '製品', number int(11) NOT NULL COMMENT '注文数', created datetime NOT NULL COMMENT '登録日', updated datetime NOT NULL COMMENT '更新日', PRIMARY KEY (id), KEY pd_order_id (pd_order_id,product_id), KEY product_id (product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='注文明細'; -- -- テーブルのデータをダンプしています 'order_details' -- INSERT INTO order_details (id, pd_order_id, product_id, number, created, updated) VALUES(1, 1, 3, 2, '2009-10-09 15:32:16', '2009-10-09 15:32:16'); INSERT INTO order_details (id, pd_order_id, product_id, number, created, updated) VALUES(2, 1, 1, 1, '2009-10-09 15:32:16', '2009-10-09 15:32:16'); INSERT INTO order_details (id, pd_order_id, product_id, number, created, updated) VALUES(3, 2, 1, 5, '2009-10-09 15:33:07', '2009-10-09 15:33:07'); INSERT INTO order_details (id, pd_order_id, product_id, number, created, updated) VALUES(4, 2, 2, 1, '2009-10-09 15:33:07', '2009-10-09 15:33:07'); -- -------------------------------------------------------- -- -- テーブルの構造 'pd_orders' -- CREATE TABLE pd_orders ( id int(11) NOT NULL auto_increment COMMENT 'ID', order_date date NOT NULL COMMENT '注文日', user_id int(11) NOT NULL COMMENT 'ユーザ', created datetime NOT NULL COMMENT '登録日', updated datetime NOT NULL COMMENT '更新日', PRIMARY KEY (id), KEY user_id (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='注文'; -- -- テーブルのデータをダンプしています 'pd_orders' -- INSERT INTO pd_orders (id, order_date, user_id, created, updated) VALUES(1, '2009-10-09', 1, '2009-10-09 15:29:56', '2009-10-09 15:29:56'); INSERT INTO pd_orders (id, order_date, user_id, created, updated) VALUES(2, '2009-10-09', 2, '2009-10-09 15:32:40', '2009-10-09 15:32:40'); -- -- ダンプしたテーブルの制約 -- -- -- テーブルの制約 `order_details` -- ALTER TABLE `order_details` ADD CONSTRAINT order_details_ibfk_1 FOREIGN KEY (pd_order_id) REFERENCES pd_orders (id), ADD CONSTRAINT order_details_ibfk_2 FOREIGN KEY (product_id) REFERENCES products (id); -- -- テーブルの制約 `pd_orders` -- ALTER TABLE `pd_orders` ADD CONSTRAINT pd_orders_ibfk_1 FOREIGN KEY (user_id) REFERENCES users (id);
・・・面倒くさいのでphpMyAdminからエクスポートで出しました(汗)
で、このDBをEclipseで見れるようにして、JPAのEntityクラスを生成します。
package testweb.entity; import java.io.Serializable; import javax.persistence.*; import java.math.BigDecimal; import java.util.Date; import java.util.List; /** * The persistent class for the products database table. * */ @Entity @Table(name="products") public class Product implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id; @Temporal( TemporalType.TIMESTAMP) private Date created; private String name; private BigDecimal price; @Temporal( TemporalType.TIMESTAMP) private Date updated; @OneToMany(mappedBy = "product") private List<OrderDetail> orderDetails; (プロパティ略) }
package testweb.entity; import java.io.Serializable; import javax.persistence.*; import java.util.Date; import java.util.List; /** * The persistent class for the users database table. * */ @Entity @Table(name="users") public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id; private String address; @Temporal( TemporalType.TIMESTAMP) private Date created; private String name; @Enumerated private Sex sex; @Temporal( TemporalType.TIMESTAMP) private Date updated; //bi-directional many-to-one association to PdOrder @OneToMany(mappedBy="user") private List<PdOrder> pdOrders; (プロパティ略) }
package testweb.entity; import java.io.Serializable; import java.math.BigDecimal; import javax.persistence.*; import java.util.Date; import java.util.List; /** * The persistent class for the pd_orders database table. * */ @Entity @Table(name="pd_orders") public class PdOrder implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id; @Temporal( TemporalType.TIMESTAMP) private Date created; @Temporal( TemporalType.DATE) @Column(name="order_date") private Date orderDate; @Temporal( TemporalType.TIMESTAMP) private Date updated; //bi-directional many-to-one association to User @ManyToOne(fetch=FetchType.LAZY) private User user; @OneToMany(mappedBy = "pd_order") private List<OrderDetail> orderDetails; (プロパティ略) public BigDecimal getTotalPrice() { BigDecimal ret = new BigDecimal(0); if (orderDetails != null) { for (OrderDetail od : orderDetails) { ret = ret.add(od.getProduct().getPrice().multiply(new BigDecimal(od.getNumber()))); } } return ret; } }
package testweb.entity; import java.io.Serializable; import javax.persistence.*; import java.util.Date; /** * The persistent class for the order_details database table. * */ @Entity @Table(name="order_details") public class OrderDetail implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int id; @Temporal( TemporalType.TIMESTAMP) private Date created; private int number; @ManyToOne(fetch = FetchType.LAZY) private PdOrder pd_order; @ManyToOne(fetch = FetchType.LAZY) private Product product; @Temporal( TemporalType.TIMESTAMP) private Date updated; (プロパティ略) }
作ったときに、外部キー制約をつけ忘れていたところがあったので、そこは手書きで書きましたが、それ以外はほとんどEclipseが自動生成してくれました。PdOrderテーブルにだけ、注文の合計値を返す独自のメソッドを定義しています。それにしても、EclipseのEntity作成機能も随分賢くなりましたね。テーブル名が複数形だったら自動的に単数形のクラスを作ってくれるのは、Railsの規約をかなり意識してるのかな?
次は、Criteriaで使う為のメタモデルクラスを作成します。現時点ではこのクラスを自動作成してくれるツールは無いみたいなので、手書きで作ってみました。
package testweb.entity; import java.math.BigDecimal; import java.util.Date; import javax.persistence.metamodel.ListAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.StaticMetamodel; @StaticMetamodel(Product.class) public class Product_ { public static volatile SingularAttribute<Product, Integer> id; public static volatile SingularAttribute<Product, Date> created; public static volatile SingularAttribute<Product, String> name; public static volatile SingularAttribute<Product, BigDecimal> price; public static volatile SingularAttribute<Product, Date> updated; public static volatile ListAttribute<Product, OrderDetail> orderDetails; }
package testweb.entity; import java.util.Date; import javax.persistence.metamodel.ListAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.StaticMetamodel; @StaticMetamodel(User.class) public class User_ { public static volatile SingularAttribute<User, Integer> id; public static volatile SingularAttribute<User, String> address; public static volatile SingularAttribute<User, Date> created; public static volatile SingularAttribute<User, String> name; public static volatile SingularAttribute<User, Sex> sex; public static volatile SingularAttribute<User, Date> updated; public static volatile ListAttribute<User, PdOrder> pdOrders; }
package testweb.entity; import java.util.Date; import javax.persistence.metamodel.ListAttribute; import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.StaticMetamodel; @StaticMetamodel(PdOrder.class) public class PdOrder_ { public static volatile SingularAttribute<PdOrder, Integer> id; public static volatile SingularAttribute<PdOrder, Date> created; public static volatile SingularAttribute<PdOrder, Date> orderDate; public static volatile SingularAttribute<PdOrder, Date> updated; public static volatile SingularAttribute<PdOrder, User> user; public static volatile ListAttribute<PdOrder, OrderDetail> orderDetails; }
package testweb.entity; import java.util.Date; import javax.persistence.metamodel.SingularAttribute; public class OrderDetail_ { public static volatile SingularAttribute<OrderDetail, Integer> id; public static volatile SingularAttribute<OrderDetail, Date> created; public static volatile SingularAttribute<OrderDetail, Integer> number; public static volatile SingularAttribute<OrderDetail, PdOrder> pd_order; public static volatile SingularAttribute<OrderDetail, Product> product; public static volatile SingularAttribute<OrderDetail, Date> updated; }
仕様書ちゃんと読んでなくて、ソース例見ながら適当に作りました(汗)どうやら一対多等のList型のプロパティはListAttributeで、Entityも含めて単数系のプロパティならSingularAttributeで定義すればいいみたいですね。
ではいよいよ、JPA2.0のCriteriaを試してみたいと思います。今回は、オーダ一覧をユーザ名で検索し、一覧表示の中に注文金額合計を出すような表を取得してみることにします。
package testweb.ejb; import java.util.List; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Root; import testweb.entity.OrderDetail_; import testweb.entity.PdOrder; import testweb.entity.PdOrder_; import testweb.entity.User_; /** * Session Bean implementation class PdOrderBean */ @Stateless public class PdOrderBean { @PersistenceContext private EntityManager em; public List<PdOrder> getPdOrders(String name) { CriteriaBuilder qb = em.getCriteriaBuilder(); CriteriaQuery<PdOrder> cq = qb.createQuery(PdOrder.class); Root<PdOrder> order = cq.from(PdOrder.class); order.fetch(PdOrder_.user, JoinType.LEFT); order.fetch(PdOrder_.orderDetails, JoinType.LEFT) .fetch(OrderDetail_.product, JoinType.LEFT); cq.where(qb.like(order.join(PdOrder_.user).get(User_.name), "%" + name + "%")); return em.createQuery(cq).getResultList(); } }
えっと・・・めんどくさい・・・(汗)
CriteriaBuilder qb = em.getCriteriaBuilder();
EntityManagerからCriteriaBuilder(つい最近、QueryBuilderから名前が変わったようです)を取得。
CriteriaQuery<PdOrder> cq = qb.createQuery(PdOrder.class);
CriteriaBuilderからCriteriaQueryを取得。
ここで渡したクラスが、FROM句のルートとして定義されます。
Root<PdOrder> order = cq.from(PdOrder.class);
CriteriaQueryのfromメソッドを使って、Rootオブジェクトを取得。このRootオブジェクトを使ってJOINやFETCH JOIN関連の定義を行います。
CriteriaQuery作るときにRootのEntityクラスを既に渡しているのに、ここでも渡すのは二度手間ですね・・・
order.fetch(PdOrder_.user, JoinType.LEFT);
FETCH JOINの定義。ここでは、オーダに多対一で紐づくユーザテーブルのJOINを定義しています。メソッドに渡している引数は、あらかじめ作成しておいたメタモデルクラスのフィールドです。
order.fetch(PdOrder_.orderDetails, JoinType.LEFT) .fetch(OrderDetail_.product, JoinType.LEFT);
オーダ詳細と、その詳細に紐づく製品情報をFETCH JOINします。fetchやjoinの定義はこのようにメソッドチェーンで書ける模様。
cq.where(qb.like(order.join(PdOrder_.user).get(User_.name), "%" + name + "%"));
CriteriaQueryのwhereメソッドを使ってWHERE句を定義します。あらかじめ作成しておいたRootオブジェクトから、関連するテーブルをjoinメソッドで定義し、更にgetメソッドによって対象となるフィールドを決定します。ここでもメタモデルクラスを引数に利用しています。
return em.createQuery(cq).getResultList();
こうやって定義したCriteriaQueryをEntityManagerに渡してやれば、後は今まで通りのやり方でデータを取得出来ます。
うーん・・・一時的な変数使い過ぎ・・・全然流れるようなインターフェイスにできない・・・
S2JDBCやLINQのメソッド構文なら、これら全てを流れるようなインターフェイスで書くので、一時的な変数が不要になって記述が激減するんですけど。これらに比べてJPAのCriteriaは、あまりにも冗長な気がします。HibernateのCriteriaだって、タイプセーフではないものの、ある程度流れるようなインターフェイスを意識した作りになっていたのに、これではかなりの退化な気が・・・
まぁとりあえず、これを実行してみると・・・
詳細レベル (低): SELECT t1.ID, t1.CREATED, t1.UPDATED, t1.order_date, t1.USER_ID, t0.ID, t0.CREATED, t0.UPDATED, t0.NUMBER, t0.PD_ORDER_ID, t0.PRODUCT_ID, t2.ID, t2.PRICE, t2.CREATED, t2.UPDATED, t2.NAME, t3.ID, t3.SEX, t3.ADDRESS, t3.CREATED, t3.UPDATED, t3.NAME FROM pd_orders t1 LEFT OUTER JOIN order_details t0 ON (t0.PD_ORDER_ID = t1.ID) LEFT OUTER JOIN products t2 ON (t2.ID = t0.PRODUCT_ID), users t3 WHERE ((t3.NAME LIKE ?) AND (t3.ID = t1.USER_ID)) bind => [%吉田%]
FETCH JOINで一気にデータを取得できていますね。JPQLと同じ動きです。ちなみに、一対多のデータをFETCH JOINで取得しても、EclipseLinkはちゃんと一つのPdOrderオブジェクトを返してくれました。JPA用にDISTINCTを定義する必要はないみたいですね。(ただし、現時点ではWHERE句が無いFETCH JOINを使ったクエリが上手く動かないようです。開発途中のライブラリなので深くは追いませんが)
ちょっと触ってみた感想を言うと・・・うーん・・・タイプセーフに書けるとはいえ、記述量が多すぎて微妙な感じです。これはインターフェイスの考え方の問題なので、JPAのCriteriaがこうと決まってしまったからには、将来にわたって改善されることも無い気がします。
昨今の状況を見てると、Javaが今後力を発揮する分野って、サーバサイドではDB周りの部分、ロジックに近い部分が主になるのではと思っているのですが、その分野に関する標準APIがどうも時代遅れになってしまっている感が・・・まぁCriteria機能としては一通り網羅しているっぽいので、ライブラリが完成すればWeb系の検索画面とかでそれなりに役に立つのでしょうけど。