Java Persistence API(TopLink Essentials)をJavaSE6で使ってみる。
EJBではEntityBean(BMP)を書いてきた身であるが、少し試しただけのHibernateの時代をすっとばして、いまやJPAの時代になってしまった。
そこでJPAを試してみることにする。
用意するもの
Apache DerbyはJDK6に付属しているので、それを使う。
TopLinkはOracleのページからダウンロードする。
落としてきたのはglassfish-persistence-installer-v2-b22.zip。
これは単純なzipではなく、実行してライセンスに同意して、はじめてjarファイルが得られる実行可能ZIPというべきものか。
すると、
が得られる。
これと、derbyのjarを含めてプロジェクトのクラスパスに追加する。
derbyはJDK6のdbフォルダ以下にある。
- derby.jar
persistence.xmlの定義
JPAは、まず、EntityManagerFactoryを得るところからはじめるが、その実装を選択するための設定ファイルが「persistence.xml」である。
これは、「classes/META-INF/persistence.xml」というような場所に配置する必要がある。(クラスパスのルートではないことに注意。)
Java Persistence API仕様書より
6.2 Persistence Unit Packaging
A persistence unit is defined by a persistence.xml file. The jar file or directory whose META-INF
directory contains the persistence.xml file is termed the root of the persistence unit. In Java EE,
the root of a persistence unit may be one of the following:
- an EJB-JAR file
- the WEB-INF/classes directory of a WAR file.(The root of the persistence unit is the WEB-INF/classes directory; the persistence.xml file is therefore contained in the WEB-INF/classes/META-INF directory.)
- a jar file in the WEB-INF/lib directory of a WAR file
- a jar file in the root of the EAR
- a jar file in the EAR library directory
- an application client jar file
persistence.xmlは、以下のような感じになる。
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <!-- EntityManagerの設定、 ローカルリソース(非JTA)のみ使用することを明示。 --> <persistence-unit name="JPATestUnit" transaction-type="RESOURCE_LOCAL"> <!-- TopLinkを使うことを指示 --> <provider> oracle.toplink.essentials.ejb.cmp3.EntityManagerFactoryProvider </provider> <!-- 明示的に@Entityクラスを登録せずに利用可能とする --> <exclude-unlisted-classes>false</exclude-unlisted-classes> <!-- TopLinkへの設定 --> <properties> <!-- ログレベル --> <property name="toplink.logging.level" value="FINEST" /> <!-- 利用するデータベースタイプ --> <property name="toplink.target-database" value="Derby" /> <!-- 利用するドライバクラス --> <property name="toplink.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" /> <!-- 接続文字列 --> <property name="toplink.jdbc.url" value="jdbc:derby:testdb" /> </properties> </persistence-unit> </persistence>
動きがよく分かるように、ログレベルをFINESTに指定している。
ログレベルはJDKのロガーの指定に従う。(log4j系ではないのでDEBUGなどはない。)
エンティティの定義
エンティティは単に@Entityの属性をつけただけの単純なクラス(Plain Old Java Object)。
最小例は、以下のような感じになるようだ。
import javax.persistence.Entity; import javax.persistence.Id; @Entity public class TestTbl { @Id private int id; private String val; /** * このデフォルトコンストラクタは、 * EntityManagerから取得する場合に必要になる。 */ private TestTbl() { super(); } public TestTbl(final int v_id) { super(); this.id = v_id; } public int getId() { return this.id; } public void setVal(final String v_value) { this.val = v_value; } public String getVal() { return this.val; } }
上記例ではメンバ変数idがprivateでsetterメソッドが存在しないが、
アノテーションの黒魔術の威力によりプライベートなメンバ変数に直接読み書きされるので、getter/settgerがなくてもJPAとの連携は可能。
気持ち悪いが、privateのデフォルトコンストラクタさえあれば大丈夫っぽい。
特に指定しないとクラス名がテーブル名となり、メンバ変数名がカラム名となる。
変更する場合はアノテーション「@Table」「@Column」のname属性で変更可能。
データベースの準備
persistence.xmlの接続文字列を「jdbc:derby:testdb」としているので、プロジェクトの真下に「testdb」を作成してやる必要がある。
JDK6のderbytools.jarのijツールを使ってデータベースの構築とテーブルの準備をする。
でijを起動して、
driver 'org.apache.derby.jdbc.EmbeddedDriver';
connect 'jdbc:derby:testdb;create=true';
でデータベースを作成しつつ接続。
CREATE TABLE TESTTBL(ID INTEGER PRIMARY KEY, VAL VARCHAR(512));
でテーブルを作成。
とりあえず、
exit;
でijを抜ける。
テストコード
設定とテーブルの準備ができたので、実際に動かしてみる。
public static void main(final String[] args) throws Exception { // META-INF/persistence.xmlの設定項目「JPATestUnit」を取得 final EntityManagerFactory entMgrFactory = Persistence.createEntityManagerFactory("JPATestUnit"); // EntityManagerを構築 final EntityManager entMgr = entMgrFactory.createEntityManager(); // ローカルトランザクション(非JTA)を開始 final EntityTransaction tx = entMgr.getTransaction(); tx.begin(); try { for (int idx = 0; idx < 100; idx++) { // PKで既存レコードを検索・取得 TestTbl rec = entMgr.find(TestTbl.class, idx); if (rec == null) { // なければ明示的にIDを振ってインスタンスを構築 // これはpersist()を呼び出すまでは一時オブジェクトである。 rec = new TestTbl(idx); rec.setVal(""); // 永続オブジェクトとして関連づける entMgr.persist(rec); } rec.setVal("*" + rec.getVal()); } tx.commit(); } finally { if (tx.isActive()) { tx.rollback(); } } }
Persistence#createEntityManagerFactory()でpersistence.xmlに設定したユニット名を指定する。
なお、指定した名前が存在しないか、persistence.xmlが間違えた場所にあったりすると、ここで何故かNullPointerException例外などという大変分かりにくいエラーが発生する。
JavaSEで動かすのでJTAは使わないであろうから、明示的にトランザクションを取得して自力で制御している。
persistence.xmlでRESOURCE_LOCALと指定していることに注意。
このコードは実行するたびに100個のレコードを作成または更新する。更新するたびにVALカラムに「*」が増えてゆくだけのものである。
まだ詳しく調べていないが、JPA自身はカラムの制約については関与も制御もしていないようで、仮に@Columnアノテーションで設定したとしても、実際にデータベースに書き込みにいったときに(長さやNotNull制約も含めて)制約違反が判明するようである。
自動発番を使ってみる
上記の例ではPKはプログラム側で勝手に振っていたが、JPAには自動的に値を取得して割り当てる機能があるので、これを使ってみる。
しかし、DerbyにはOracleのSEQUENCEのようなものがないらしいので、発番テーブルを自前で用意して制御する必要がある。(2006/12/17追記: 間違いでした。)*1
ここではPKを自動発番とするためにテーブル上でID値を管理する。
その場合のエンティティは以下のような感じとなる。(ついでにクエリーも定義している。)
import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Column; import javax.persistence.Table; import javax.persistence.TableGenerator; import javax.persistence.GeneratedValue; import javax.persistence.NamedQuery; @Entity @NamedQuery( name="findByName", query="SELECT o FROM TestTbl2 o WHERE o._name = :NAME") @Table(name="TESTTBL2") public class TestTbl2 { @Id @Column(name="ID") @TableGenerator( name="testtbl2seq", table="ID_GEN", pkColumnName="NAME", valueColumnName="SEQ", pkColumnValue="TESTTBL2") @GeneratedValue( strategy=javax.persistence.GenerationType.TABLE, generator="testtbl2seq") private int _id; @Column(name="NAME") private String _name; @Column(name="VAL") private String _value; public TestTbl2() { super(); } public int getId() { return this._id; } public void setName(final String v_name) { this._name = v_name; } public String getName() { return this._name; } public void setValue(final String v_value) { this._value = v_value; } public String getValue() { return this._value; } }
IDは自動発番されるので、コンストラクタを含めてsetterは必要なくなる。
@GeneratedValueアノテーションで、このメンバ変数が自動割り当てされることを示し、その属性でテーブルを使った発番であることを示している。
その発番の詳細が、@TableGeneratorアノテーションによって定義されている。
この自動発番では、以下のテーブルを使う。
CREATE TABLE ID_GEN (NAME VARCHAR(64) PRIMARY KEY, SEQ INTEGER NOT NULL);
INSERT INTO ID_GEN (NAME, SEQ) VALUES ('TESTTBL2', 0);
注意することは、最初に「TESTTBL2」という主キーをもつSEQが0のレコードを登録していること。
@TableGeneratorアノテーションで、ID_GENテーブルのNAMEカラムをキーとして「TESTTBL2」という名前を用いることが指示されているが、このレコードは予め存在しないと発番することができない。
TESTTBL2自身は、以下のように作成しておく。
CREATE TABLE TESTTBL2 (ID INTEGER PRIMARY KEY, NAME VARCHAR(64) NOT NULL UNIQUE, VAL VARCHAR(64));
テストコード2
public static void main(final String[] args) throws Exception { // META-INF/persistence.xmlの設定項目「JPATestUnit」を取得 final EntityManagerFactory entMgrFactory = Persistence.createEntityManagerFactory("JPATestUnit"); // EntityManagerを構築 final EntityManager entMgr = entMgrFactory.createEntityManager(); // ローカルトランザクション(非JTA)を開始 final EntityTransaction tx = entMgr.getTransaction(); tx.begin(); try { // クエリ final Query query = entMgr.createNamedQuery("findByName"); for (int loop = 0; loop < 1000; loop++) { final int no = (int)(Math.random() * 500); final String name = String.format("%03d", new Object[]{no}); // クエリのバインド変数にセットし、クエリを実行する query.setParameter("NAME", name); final List lst = query.getResultList(); if (lst.isEmpty()) { // 新規に一時インスタンスを作成する。 // IDは自動 final TestTbl2 rec = new TestTbl2(); rec.setName(name); rec.setValue("-"); entMgr.persist(rec); } else { // 取得された最初のアイテムを更新する。 final TestTbl2 rec = (TestTbl2) lst.get(0); rec.setValue("-" + rec.getValue()); } } tx.commit(); } finally { if (tx.isActive()) { tx.rollback(); } } }
これは、実行するたびにランダムに番号を選んで名前とし、その名前が未登録ならば追加、そうでなければ更新する。更新するたびにVALカラムに「-」が増えてゆくだけのものである。
クエリはEntityManagerから作成できるが、エンティティクラス側で定義することができるので、こちらを使ったほうがラクな気がする。
定義されたクエリは「createNamedQuery()」で取得でき、そのあとでバインド変数を設定できる。
クエリは常にリストで返してくるが、このケースではNAMEカラムはユニーク制約になっているので0 or 1なことは明白。
よって、空でなければ最初のレコードのみ取得し更新、空であれば新規に作成して登録、という処理となっている。
結論
まだリレーションを試していないが、幸先良いような気がしてきた。
アノテーションを使った記述方法も含めてJPAとして規格化されたこと、しかも、JavaEEだけでなくJavaSEでも共通で使えるというのは、ちょっと試そうかなと思ったあとに発生する心理的な「無駄になるかもしれない早期徒労感」を下げてくれていると思う。
たしかにDerbyとTopLink Essentialで、ほどよい感じに手軽にO/R Mappingが使えるようになってきたような感じがする。
*1:2006/12/17追記: Derbyには自動発番するカラムを宣言できる。http://db.apache.org/derby/docs/10.1/ref/rrefsqlj37836.html これによると、CREATE TABLE tablename (pk_column INT GENERATED ALWAYS AS IDENTITY, ...)のようにDDLを書き、INSERT文などのDMLではpk_columnを省略することで自動的に一意となるキーを割り当ててくれる。