エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JavaEE7をはじめよう(2) - JPAの基本

JPAJava Persistence API)は、データベースなどの永続化記憶とJavaのオブジェクトとのマッピングを定義して、SQLをあまり書くことなくデータベースとの同期を取ることができる仕組みである。

JPAはO-Rマッピングだけでなく、永続化コンテキストによる同期化やクエリAPIなど多くの機能を備えており、これらの機能を使うことで、SQLを含むコード量の削減やパフォーマンス向上などの多くのメリットが得られる。

この記事では、JPAの詳細な仕様に立ち入らずに、JPAの概要や使用方法、知っておくとよいトピックについて紹介する。

概要

JPAの主な機能は以下の通り

  • O-Rマッピング - Javaオブジェクトにアノテーション(またはXMLファイル)を指定して、データベースのテーブルとのマッピングを定義する。オブジェクトとテーブルを1対1でマッピングするだけでなく、テーブル間の関連の多重度、外部キー、一意性制約など様々な情報を付与できる。

  • エンティティマネージャ - 上記で情報を付与したオブジェクトをエンティティと呼ぶ。エンティティマネージャはエンティティを管理し、CRUDなどの操作や後述する永続化コンテキストの管理を行う。

  • クエリAPI - 条件に合致するエンティティの検索など、主キー以外の検索を行うためのAPI
    クエリの種類は以下の3つがある。クエリの定義はアノテーションで固定文字列で持つか、プログラム中の文字列またはCriteria API で作成する。

    • JPQL - JPA標準の問い合わせ言語。SQLでテーブル名を各場所にエンティティクラス名を書くなど、SQLと少々異なる部分があるが、SQLを知っていれば理解は容易である。ベンダー間の差異を吸収し、SQLに翻訳される。
    • NativeQuery - SQL を直接記述する方式。例えば、JPQLに存在しない、ベンダー固有の関数などを利用できる。
    • Criteria API - 上記2種は文字列によってクエリを組み立てるが、Criteria APIはクエリオブジェクトに、 fromwhere などのメソッドを呼び出すことによってクエリを構築する。一般的に動的なクエリ構築に使用する。
  • コールバック・リスナ - データベースのトリガのように、エンティティの生成・更新などのタイミングで特定の処理を差し込むことができる仕組み。

  • トランザクション - 上記エンティティの操作をトランザクション内で実行し、ロールバックやコミットを行う仕組み。(厳密にはJTAの機能)

また、JPAJava EEだけを前提とした仕様ではなく、APIと実装のjarファイルを導入すれば Java SEでも実行可能だ。
これは、スタンドアローンな環境でもJPAを利用できること意味する。このため、データベースを使用するテストも、Java EE 実行環境なしで実行できる。

使用手順

JPAのライブラリがあることを前提として、JPAを使用するために用意するものは以下の2つである。

  • persistence.xml - データベースの接続定義や各種オプションを記述する。クラスパス上に配置すれば自動で読み込まれる。
  • エンティティクラス - @Entity および IDアノテーション@Id, @EmbeddedId)を付与したJavaクラス

今回のサンプルではエンティティの定義をアノテーションでのみ行う(実際、アノテーションベースの方がオブジェクトに直接定義が記述されるので理解しやすい)。

サンプル

以降では、サンプルを通じて、O-Rマッピング、永続化コンテキスト、クエリについて解説する。 このサンプルはJUnitでテスト可能な構成とし、組み込みデータベースとして、h2 を使用する。

エンティティの構成

スポーツなどの団体名を持つチームと、そのチームに所属するメンバーという、1対多の関連を持つエンティティを取り扱う。

f:id:enterprisegeeks:20141208175011p:plain

定義ファイルの設定

persistence.xmlを以下のように作成し、META-INFに配置する。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="ut" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <!-- 実行時にテーブルの生成をメタデータ(エンティティ)から行う。 -->
      <property name="javax.persistence.schema-generation.database.action" value="create"/>
      <property name="javax.persistence.schema-generation.create-source" value="metadata"/>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test"/>
    </properties>
  </persistence-unit>
</persistence>

アプリケーションサーバーで実行する場合はJNDIのデータソースを指定するが、上記のように通常のJDBC接続でも動作する。 プロパティのうち、重要なものを解説する。

スキーマ生成プロパティ

javax.persistence.schema-generation.database.actionプロパティを使用すると、 プログラム実行時やWEBアプリケーションのデプロイ時に、データベースのテーブルを自動生成する。
javax.persistence.schema-generation.create-source 生成の元情報としてmetadata(エンティティクラスのアノテーション)やddlファイル名を指定できる。 また、 javax.persistence.sql-load-script-source でテーブル生成後に初期化用のSQLを実行することも可能であり、これを使うことで初期データの投入が可能だ。

他のプロパティは、 以下に詳細があり、ドロップ時の挙動なども指定可能となっている。 37.5 Database Schema Creation - Java Platform, Enterprise Edition: The Java EE Tutorial (Release 7)

スキーマ生成プロパティを使うと、揮発性の組み込みDBを用いたJUnitのテストで毎回自動でテーブルを作ることが可能だ。
Webアプリケーションのデプロイでも手作業なしでテーブルを作ることができる。また、ドロップのSQLも組み合わせることで、エンティティが変更された場合に、テーブルをドロップして新しいエンティティの内容でテーブルの新規作成することでエンティティの変更をデータベースに反映できる。

Ruby on Rails のDBマイグレーションほど強力ではないが、スキーマ生成を使用することで、データベースの構築が簡潔になる。

エンティティの生成

エンティティクラスは、先ほどの論理ER図に合わせて以下のように定義する。

@Entity
// 一意制約を設定
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "name"))
public class Team implements Serializable {
    /** ID(自動付与) */
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    /** チーム名 */
    @Column(length = 30)
    private String name;
    /** 所属メンバー */
    // cascadeにより、関連エンティティ(Member)へ永続化のライフサイクルをあわせることを設定
    @OneToMany(mappedBy = "belongs", cascade = CascadeType.ALL)
    private List<Member> members;
    /** バージョン番号 */
    @Version // 楽観排他
    private long version;
    /** 更新日(監査項目) */
    @Temporal(TemporalType.TIMESTAMP)
    private Date updated;
    
    // 変更時の自動設定
    @PrePersist @PreUpdate
    private void now(){
        setUpdated(new Date());
    }
// getter, setterは割愛
}
@Entity
// 選手番号はチームごとにユニークである。
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"PLAYER_NUMBER", "belongs_id"}))
public class Member implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    /** 選手番号 */
    @Column(name="PLAYER_NUMBER",scale = 4)
    private int playerNumber;
    /** 選手名 */
    @Column(length = 30)
    private String name;
    /** 所属チーム */
    @ManyToOne
    private Team belongs;
    @Version // 楽観排他
    private long version;
    /** 更新日(監査項目) */
    @Temporal(TemporalType.TIMESTAMP)
    private Date updated;

    // 変更時の自動設定
    @PrePersist @PreUpdate
    private void now(){
        setUpdated(new Date());
    }
}
// getter, setterは割愛

アノテーションの詳細は次回以降の記事で解説するが、以下のような考慮を加えた。

  • 代理キーとしてid(自動採番)を使用し、論理ER図の主キーはユニーク制約として@Tableで定めている。
  • 楽観排他用の項目 version およびアノテーション@version を加えた。
  • 監査項目として登録・更新のたびにシステム日時を設定する項目 updated とコールバック now() を加えた。
  • チームから見たメンバーへの1対多の関連は@OneToMany、メンバーから見たチームへの多対1の関連は@ManyToOneで表した。
テストの実行

以下の通り、JUnitによるテスト実行が可能となる。

public class JPATest {
    
    private static EntityManager getEm() {
        return Persistence.createEntityManagerFactory("ut")
                          .createEntityManager();
    }
    
    @Test 
    public void テスト(){
        Team t1 = new Team();
        t1.setName("チームA");
        
        Member m1 = new Member();
        m1.setPlayerNumber(1);
        m1.setName("member1");
        m1.setBelongs(t1);
        
        t1.setMembers(Arrays.asList(m1));
        
        // エンティティマネージャの取得とトランザクション開始
        EntityManager em = getEm();
        em.getTransaction().begin();
        // 永続化コンテキストへの登録
        em.persist(t1);
        // データベースへの反映,コンテキスト破棄
        em.flush();
        em.clear();
        // DBから再取得
        Team act = em.find(Team.class, t1.getId());
        assertThat(act.getName(), is("チームA"));
        // 関連先の情報も取得可能。
        assertThat(act.getMembers().get(0).getName(), is("member1"));
        
        em.getTransaction().commit();
    }
}

今回のケースでは、以下のテーブルが実際に生成されている。 (データベースによって異なるが、h2の場合はIDの連番を生成するためのSEQUENCEというテーブルも内部で生成される)。

f:id:enterprisegeeks:20141208184556p:plain

ここではDDLSQLを一切書かずに、テーブルの生成と1件単位のCRUDが可能となっている。 また、idversion,updatedはプログラムからは明示していないが、 内部的に適切な値が設定されテーブルに挿入されている。

まとめ

今回は、関連を持つテーブルのサンプルを通して JUnit上でJPAを実行した。

次回以降の記事では、このサンプルを掘り下げて、永続化コンテキスト、クエリ、JPAの効果的な使い方を解説していく。

[前多 賢太郎]