OpenJPA は、Java EE 5で導入されたEJB3.0(JSR220 )のうち、Java Persistence API(JPA)を実装したオープンソースプロダクトです。2007年8月28日にバージョン1.0.0がリリースされました(1.0.0のアナウンス )。配布はApacheライセンスの下で行われています。
これが組み込まれているプロダクトには、Spring Framework やApache Geronimo があります。Apache Geronimoは、2007年8月20日にリリースされたバージョン2.0.1がJava EE 5互換の認定を受けています(リリース時のアナウンス )。
また、OpenJPAは商用製品のBEA Kodo のベースにもなっています。というよりも、BEA KodoにおけるJPAの実装が2006年2月にApache Software Foundationに寄贈されたもの がOpenJPAなのです。トップレベルプロジェクトに昇格したのは2007年05月16日です。
Kodoは、もともとSolarMetricという企業が開発していた製品で、RDBとJavaプログラムとの間でデータをやりとりするO/Rマッピングの仕様であるJava Data Objects(JDO) (JSR12 )を実装したものでした。この企業をアプリケーションサーバWebLogicで知られるBEA が2005年11月に買収し、EJB3.0の実装がKodoに組み込まれました。それが現在のBEA Kodoです。
オープンソースのプロダクトは、もともと技術者のボランティアで開発が進められていたものという認識がありましたが、最近では、商用製品(とくにその基盤部分)をオープンソースで開発するプロジェクトが増えてきています。そうしたプロジェクトでは、商用製品を開発する企業がメンバーを送り込んで主体的に開発を進めるという体制ができあがっています。つまりそのメンバーは、報酬を得ながらオープンソースプロダクトを開発しているのです。ですから、もはやオープンソース=ボランティアとも言い切れない状態になっているのです。
Java Persistence API(JPA)とは
Java Persistence API(JPA)とは、Enterprise JavaBeans(EJB)のうち、データベースとのやり取りを行う永続化処理の部分をEntity Beanから分離したものです。これにより、Java標準のO/Rマッピングを、EJBコンテナ以外の一般的なJava環境(JDK 5以上)でも実行できるようになりました。
仕様の策定は、O/RマッピングツールHibernate の開発者Gavin King氏を中心として行われ、2006年5月11日に仕様がリリースされました。ちなみに、HibernateによるJPAの実装は、Hibernate Annotations とHibernate EntityManager に組み込まれています。
特徴は、EntityManagerにデータのやり取りを行うメソッドを集中させていること、JDK 5で導入されたアノテーションをふんだんに取り入れていること、EJB QL という問い合わせ言語が以前のバージョンのEJBよりも強化されたことなどが挙げられます。
OpenJPAによるO/Rマッピング
では、実際にOpenJPAを用いてO/Rマッピングを行うことにしましょう。
O/Rマッピング とは、Javaプログラム(Object)とRDBMS(Relational)との間でデータのやり取りをする際に、あたかも両者がオブジェクトのやりとりをしているように見せかけることや、そのための設定を指します。OpenJPAでは、JPAの仕様に基づいたO/Rマッピングを行うことができます。
ここから、その具体的な方法を説明していきます。以下はJDK 5以上で実行可能です。また、実行時にはOpenJPAのlibディレクトリに含まれているJARファイルのフルパスを、環境変数CLASSPATHに追加してください。そしてOpenJPAのルートディレクトリにあるopenjpa-1.0.0.jarは、javaコマンドの-javaagentオプション に指定してください。詳細はOpenJPAのドキュメント などを参照してください。
persistence.xmlによるデータベース設定
JPAにおいてO/Rマッピングを行うために欠かせないのは、persistence.xml という設定ファイルです。これには、データベースに対してJDBCによる接続を行うために必要となるドライバクラス名、接続URL、ユーザ名、パスワード を記述します。それと、データベースのテーブルに対応するエンティティクラスの名称 も合わせて記述しておきます。
リスト1 にpersistence.xmlの例を示します。このファイルは、Javaプログラム実行時のカレントディレクトリの下のMETA-INF ディレクトリに配置しておきます。
ルートタグ<persistence> の中には複数の<persistence-unit>タグ を記述できます。Javaプログラムから設定を利用されるのは<persistence-unit>のほうです。この中の<property>タグにJDBC接続などで必要な設定を記述します。これらは実行時にシステムプロパティとして設定することもできます。ここでは、OpenJPAに組み込まれているApache Derby を利用し、Javaプログラムの実行によって、テーブルが存在しなければ自動的に作成するようにしています。
リスト1 接続するデータベースに関する設定(例) - persistence.xml
<?xml version="1.0" encoding="Windows-31J"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="1.0">
<persistence-unit name="miniblog" transaction-type="RESOURCE_LOCAL">
<!-- データベースのテーブルに対応するJavaクラス -->
<class>Miniblog</class>
<!-- データベースに関する設定(JDBC関連など) -->
<properties>
<!-- ドライバクラス名 -->
<property name="openjpa.ConnectionDriverName"
value="org.apache.derby.jdbc.EmbeddedDriver"/>
<!-- 接続URL -->
<property name="openjpa.ConnectionURL"
value="jdbc:derby:openjpa-example;create=true" />
<!-- データベース接続用のユーザ名 -->
<property name="openjpa.ConnectionUserName" value="" />
<!-- データベース接続用のパスワード -->
<property name="openjpa.ConnectionPassword" value="" />
<property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema" />
<!-- ログ出力の設定(実行したSQLも出力) -->
<property name="openjpa.Log" value="DefaultLevel=WARN,SQL=TRACE" />
</properties>
</persistence-unit>
</persistence>
エンティティクラスの作成
データベースのテーブルに対応するエンティティクラスは、リスト1の<class>タグに記述されているMiniblogクラス です(リスト2 )。これはTwitter などによってメジャーになった、ミニブログに投稿される短い文字列を保持するものです。このクラスは大きく以下の4つの部分に分かれています。
1.クエリ設定
クエリ とは、データベースに対するデータの問い合わせのことです。JPAでは@NamedQueryに記述します。設定を複数記述する場合は@NamedQueries内に@NamedQueryの配列を記述します。通常クエリはSQLで記述されますが、ここではEJB QL という言語で記述されています。といっても両者はよく似ており、リスト2 に記述した程度の短かさだと、パラメータを表す:name 以外はほとんど区別がつかないほどです。
2.プロパティ
Miniblogクラスは、個別のデータを表すID(id)、ユーザ名(name)、ユーザが投稿したメッセージ(message)、データが生成された日時(createdDate) という4つのプロパティを持っています。これらはそのままテーブルが持つカラムとなります。
idプロパティの値は、@Id と@GeneratedValue によってデータごとに自動的に番号がつけられるようになっています。それ以外は@Basic によってプロパティの名称やデータ型に対応するカラムがマッピングされます。
3.コンストラクタ
エンティティクラスには、引数を持たないコンストラクタが必要です。それ以外は、Javaプログラムとして必要なものを宣言します。リスト2 では、ユーザ名とメッセージから1つのインスタンスを生成できるようにしています。
4.getter/setterメソッド
データベースにデータを保存したり、逆にデータベースからデータを取得したりする際に用いられるメソッドです。記述は単調ですが、エンティティクラスにとってはデータのやり取りをする要(かなめ)となる大事なメソッドです。
リスト2 データベースのテーブルに対応するクラス(Miniblog.java)
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
// エンティティクラスであることを示す
@Entity
// あらかじめ定型的なクエリに名前をつけておく
@NamedQueries( {
@NamedQuery(
name="findAllMessages",
query="select mb from Miniblog mb"
),
@NamedQuery(
name="findAllMessagesByName",
query="select mb from Miniblog mb where mb.name = :name"
),
@NamedQuery(
name="findAllNames",
query="select distinct mb.name from Miniblog mb order by mb.name"
)
} )
// エンティティクラス
public class Miniblog implements java.io.Serializable {
@Id
@GeneratedValue
private long id; // ID番号
@Basic
private String name; // ユーザ名
@Basic
private String message; // メッセージ
@Basic
private Date createdDate = new Date(); // データが生成された日時
public Miniblog() {} // 引数が空のコンストラクタが必要
public Miniblog( String nm, String msg ) { // 引数はユーザ名、メッセージ
setName( nm );
setMessage( msg );
}
// getterメソッド(データ取得)
public long getId() { return id; }
public String getName() { return name; }
public String getMessage() { return message; }
public Date getCreatedDate() { return createdDate; }
// setterメソッド(データ設定)
public void setId( long value ) { id = value; }
public void setName( String nm ) { name = nm; }
public void setMessage( String msg ) { message = msg; }
public void setCreatedDate( Date d ) { createdDate = d; }
}
EntityManagerの生成
persistence.xmlとエンティティクラスが揃ったら、JPAによるデータベースへのアクセスが可能になります。そのアクセスを実際に行うのはEntityManager クラスの役割です。次はこのインスタンスを生成します。
実際の手順は、EntityManagerFactoryインスタンスを生成してからEntityManagerインスタンスを生成するという2段構えになっています(リスト3 )。こうすることで、JPAを実装した個別のライブラリへの依存度をできるだけなくすようにしているのです。
このとき、EntityManagerFactoryの生成時(Persistence.createEntityManagerFactory()メソッド実行時)の第一引数"miniblog"は、persistence.xml(リスト1 )に記述されている<persistence-unit>タグのname属性の値です。この引数で、どのO/Rマッピングの設定を用いるかを指定しているのです。
リスト3 EntityManagerの生成
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
....................
EntityManagerFactory factory =
Persistence.createEntityManagerFactory(
"miniblog", System.getProperties() );
EntityManager em = factory.createEntityManager();
// ..... (省略) .....
em.close();
factory.close();
データの書き込み(保存)
データの書き込みは、EntityManager.persist() メソッドで行います(リスト4 )。データを更新する場合は同クラスのmerge()メソッドを実行します。書き込みの前後では、必ずトランザクションの開始と終了(コミット)を実行してください。こうすることによりトランザクションを単位としてロールバック(データベースを変更前の状態に戻す)ができ、例外処理などが実行しやすくなります。
リスト4 によってデータベース(Apache Derby)に対して実行されるSQLをリスト5 に示します。先頭のCREATE TABLEは、あらかじめ自動的に作成するように設定しているために実行されたものであり、これを実行しないように設定を変更することは可能です。
その後のINSERT文中に記述されている ? はパラメータマーカーで、続いて記述されている[params=~]内のデータが先頭から順に代入されてから、実際のINSERT文が実行されています。
リスト4 EntityManagerによるデータの書き込み(保存)
// 書き込むミニブログのデータ(ユーザ名、投稿する文字列)
import java.util.Date;
....................
Miniblog[] minblog = {
new Miniblog( "masanori", "ようやく過ごしやすい天気になってきたね" ),
new Miniblog( "okibayashi", "でもこれからまた暑くなるらしいよ" ),
new Miniblog( "masanori", "えぇ~、またぁ 早く涼しくならないかなぁ" ),
new Miniblog( "okibayashi", "まだしばらくは我慢しなきゃならないみたいだ" )
};
for( Miniblog mb : minblog ) {
mb.setCreatedDate( new Date() ); // 日時を修整
em.getTransaction().begin(); // トランザクション開始
em.persist( mb ); // データベースに書き込み(保存)
em.getTransaction().commit(); // トランザクション終了(コミット)
Thread.sleep( 990 ); // 投稿に時間差をつける(約1秒間)
}
リスト5 データベース(Apache Derby)に対して実行されるSQL(トレースより抜粋)
CREATE TABLE Miniblog (id BIGINT NOT NULL, createdDate TIMESTAMP, message VARCHAR(255), name VARCHAR(255), PRIMARY KEY (id))
INSERT INTO Miniblog (id, createdDate, message, name) VALUES (?, ?, ?, ?) [params=(long) 1, (Timestamp) 2007-08-31 16:33:57.448, (String) ようやく過ごしやすい天気になってきたね, (String) masanori]
INSERT INTO Miniblog (id, createdDate, message, name) VALUES (?, ?, ?, ?) [params=(long) 2, (Timestamp) 2007-08-31 16:33:59.24, (String) でもこれからまた暑くなるらしいよ, (String) okibayashi]
INSERT INTO Miniblog (id, createdDate, message, name) VALUES (?, ?, ?, ?) [params=(long) 3, (Timestamp) 2007-08-31 16:34:00.251, (String) えぇ~、またぁ 早く涼しくならないかなぁ, (String) masanori]
INSERT INTO Miniblog (id, createdDate, message, name) VALUES (?, ?, ?, ?) [params=(long) 4, (Timestamp) 2007-08-31 16:34:01.252, (String) まだしばらくは我慢しなきゃならないみたいだ, (String) okibayashi]
データの読み込み(クエリ発行)
JPAでは、クエリをQueryインスタンスで表します。これを生成するのがEntityManager.createQuery()メソッドです。引数にはEJB QLによる問い合わせを記述します。リスト2のように@NamedQueryによってクエリに名称がつけられているときは、EntityManager.createNamedQuery()メソッドにその名称を設定します。
リスト6 はシンプルなSELECTを実行した例です。この結果を取得するにはQuery.getResultList()メソッドを実行してListを取得します。ListにGenericsをつけて総称化しておけば、クエリにより取得したデータ(Listの各要素)のキャストも自動的に行われます。
リスト7 は、リスト2のクエリ中に記述されている:nameというパラメータに対してパラメータを設定する例です。ここではユーザ名が'masanori'のメッセージのみをデータベースから取得しています。リスト7が実行されるとき、その内部ではアクセス対象のRDBMSに合わせたSQLが生成/実行されます。こうすることで、Javaプログラム内にSQLを記述しなくて済むようにしています。
リスト8 は、distinctやorder byといったSQL文でよく用いられる記述(句など)をEJB QLで記述している例です。EJB QLはあくまでオブジェクトに対する問い合わせ言語なのですが、SQLと似た構文で、同じような実行結果が得られます。データベースに発行されるクエリと、リスト2の@NamedQueryで定義されているクエリにほとんど違いがないのがわかります。
リスト6 データの読み込み(クエリ発行)(例)
import java.text.SimpleDateFormat;
import java.util.List;
import javax.persistence.Query;
....................
SimpleDateFormat sdf = new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss" );
// Query q1 = em.createQuery( "select mb from Miniblog mb" );
Query q1 = em.createNamedQuery( "findAllMessages" );
for ( Miniblog mb : (List<Miniblog>) q1.getResultList() ) {
System.out.printf( "%03d %-19s %-10s - %-30s\n",
mb.getId(), sdf.format( mb.getCreatedDate() ), mb.getName(), mb.getMessage() );
}
実行結果
001 2007/08/31 16:33:57 masanori - ようやく過ごしやすい天気になってきたね
002 2007/08/31 16:33:59 okibayashi - でもこれからまた暑くなるらしいよ
003 2007/08/31 16:34:00 masanori - えぇ~、またぁ 早く涼しくならないかなぁ
004 2007/08/31 16:34:01 okibayashi - まだしばらくは我慢しなきゃならないみたいだ
リスト7 データの読み込み(パラメータ設定)(例)
// EntityManagerによるQuery生成
Query q2 = em.createNamedQuery( "findAllMessagesByName" )
.setParameter( "name", "masanori" );
データベースに発行されるクエリ
SELECT t0.id, t0.createdDate, t0.message,t0.name FROM Miniblog t0 WHERE (t0.name = ?) [params=(String) masanori]
実行結果
001 2007/08/31 16:33:57 masanori - ようやく過ごしやすい天気になってきたね
003 2007/08/31 16:34:00 masanori - えぇ~、またぁ 早く涼しくならないかなぁ
リスト8 DISTINCTやORDER BYも有効(例)
// EntityManagerによるNamedQuery生成
Query q3 = em.createNamedQuery( "findAllNames" );
データベースに発行されるクエリ
SELECT DISTINCT t0.name FROM Miniblog t0 ORDER BY t0.name ASC
実行結果
masanori
okibayashi
JPAを用いると、直接データベースを操作しなくても、データのやり取りを行うアプリケーションを構築できます。@NamedQueryによってクエリをエンティティクラスの側に記述するようにすれば、EJB QLを担当する作業者と、そうでない作業者とを明確に分けることができるようになります。
ここでは紹介できなかった複雑なリレーションを設定する方法も、JPAには多数用意されていますので、興味をもった方は、ドキュメントなどを参考にして、より難しいマッピングにチャレンジしてみてください。