気になる開発プロダクツ

第6回OpenJPA 1.0.0

OpenJPAは、Java EE 5で導入されたEJB3.0(JSR220)のうち、Java Persistence API(JPA)を実装したオープンソースプロダクトです。2007年8月28日にバージョン1.0.0がリリースされました(1.0.0のアナウンス)。配布はApacheライセンスの下で行われています。

これが組み込まれているプロダクトには、Spring FrameworkApache 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 AnnotationsHibernate 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には多数用意されていますので、興味をもった方は、ドキュメントなどを参考にして、より難しいマッピングにチャレンジしてみてください。

おすすめ記事

記事・ニュース一覧