seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

Java Persistence API(TopLink Essentials)をJavaSE6で使ってみる。

EJBではEntityBean(BMP)を書いてきた身であるが、少し試しただけのHibernateの時代をすっとばして、いまやJPAの時代になってしまった。
そこでJPAを試してみることにする。

用意するもの

Apache DerbyはJDK6に付属しているので、それを使う。

TopLinkOracleのページからダウンロードする。
落としてきたのはglassfish-persistence-installer-v2-b22.zip
これは単純なzipではなく、実行してライセンスに同意して、はじめてjarファイルが得られる実行可能ZIPというべきものか。

java -jar glassfish-persistence-installer-v2-b22.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ツールを使ってデータベースの構築とテーブルの準備をする。

java -cp derbytools.jar;derby.jar org.apache.derby.tools.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.xmlRESOURCE_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を省略することで自動的に一意となるキーを割り当ててくれる。