App Engine での JPA の使用

Java Persistence API(JPA)は、Java でデータベースにアクセスするための標準インターフェースであり、Java のクラスとデータベースのテーブルとの間のマッピングを自動的に実行します。データストアで JPA を使用するために利用できるオープンソースのプラグインがあります。このページでは、そのプラグインを使う方法について説明します。

警告: 多くのデベロッパーは、低水準の Datastore API を使用するか、Datastore 専用に開発された Objectify などのオープンソースの API を使用するほうが快適に作業できると思われます。JPA は、従来型のリレーショナル データベースで使用するように設計されているため、リレーショナル データベースとは異なるデータストアのいくつかの特性(エンティティ グループや祖先クエリなど)を明示的に表すことができません。これは、理解や修正が困難な細かい問題の原因になることがあります。

App Engine Java SDK にはバージョン 1.x のプラグインが含まれ、JPA バージョン 1.0 を実装しています。この実装は、DataNucleus Access Platform バージョン 1.1 に基づいています。

注: このページで説明するのは JPA バージョン 1 での手順です。このバージョンは、App Engine の DataNucleus プラグインのバージョン 1.x を使用します。DataNucleus プラグインのバージョン 2.x も使用できます。このバージョンは JPA 2.0 を使用します。2.x プラグインには新しい API や機能が数多く用意されていますが、バージョン 1.x との互換性はありません。JPA 2.0 を使用するアプリケーションを再構築する場合には、コードを更新し、再度テストを行う必要があります。新しいバージョンの詳細については、App Engine で JPA 2.0 を使用するをご覧ください。

JPA のセットアップ

データストアへのアクセスに JPA を使用するには、App Engine アプリで次のことを行う必要があります。

  • JPA およびデータストアの JAR は、アプリの war/WEB-INF/lib/ ディレクトリにある必要があります。
  • persistence.xml という名前の JPA に App Engine データストアの使用を指示する構成を持つ構成ファイルが、アプリの war/WEB-INF/classes/META-INF/ ディレクトリに必要です。
  • プロジェクトの構築プロセスで、コンパイル済みのデータクラスでコンパイル後の「拡張」手順を実行し、そのデータクラスを JPA 実装に関連付けます。

JAR のコピー

JPA とデータストアの JAR は App Engine Java SDK に含まれています。これらは appengine-java-sdk/lib/user/orm/ ディレクトリにあります。

JAR をアプリケーションの war/WEB-INF/lib/ ディレクトリにコピーします。

appengine-api.jarwar/WEB-INF/lib/ ディレクトリにあることも確認します(プロジェクトの作成時にすでにコピーしている場合もあります)。App Engine DataNucleus プラグインは、この JAR を使用してデータストアにアクセスします。

persistence.xml ファイルの作成

JPA インターフェースでは、アプリケーションの war/WEB-INF/classes/META-INF/ ディレクトリに persistence.xml という名前の構成ファイルが必要です。このファイルは、この場所に直接作成することも、ビルドプロセスでソース ディレクトリからコピーすることもできます。

作成するファイルには次の内容を含めます。

<?xml version="1.0" encoding="UTF-8" ?>
<persistence 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_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

</persistence>

読み取りポリシーとデータストア呼び出し期限

Datastore のクエリページで説明したように、読み取りポリシー(強整合性と結果整合性)と、persistence.xml ファイルの EntityManagerFactory のデータストア呼び出し期限を設定できます。これらの設定は、<persistence-unit> 要素に追加します。特定の EntityManager インスタンスで行われるすべての呼び出しは、マネージャーが EntityManagerFactory で作成されたときに選択された構成を使用することになります。個々の Query に対してこれらのオプションをオーバーライドすることもできます(下記をご覧ください)。

読み取りポリシーを設定するには、datanucleus.appengine.datastoreReadConsistency という名前のプロパティを追加します。指定できる値は、EVENTUAL(結果整合性による読み取り)と STRONG(強整合性による読み取り)です。指定しない場合、デフォルトで STRONG が適用されます。

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />

読み取りと書き込みに個別のデータストア呼び出し期限を設定できます。読み取りには、JPA の標準プロパティ javax.persistence.query.timeout を使用します。書き込みには、datanucleus.datastoreWriteTimeout を使用します。値は、ミリ秒単位の時間です。

            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />

クロスグループ(XG)トランザクションを使用する場合は、次に示すプロパティを追加します。

            <property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

異なる name 属性を使用して、同じ persistence.xml ファイルに複数の <persistence-unit> 要素を設定し、同じアプリで異なる構成の EntityManager インスタンスを使用できます。たとえば、次の persistence.xml ファイルは、"transactions-optional" という名前と "eventual-reads-short-deadlines" という名前の 2 つの構成を確立します。

<?xml version="1.0" encoding="UTF-8" ?>
<persistence 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_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

    <persistence-unit name="eventual-reads-short-deadlines">
        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />
        </properties>
    </persistence-unit>
</persistence>

指定された構成セットを用いて EntityManager を作成する方法については、後述の EntityManager インスタンスの取得をご覧ください。

Query オブジェクトごとに読み取りポリシーと呼び出し期限をオーバーライドできます。Query のクエリの読み取りポリシーをオーバーライドするには、その setHint() メソッドを呼び出します。

        Query q = em.createQuery("select from " + Book.class.getName());
        q.setHint("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

前述のように、指定できる値は "EVENTUAL""STRONG" です。

読み取りタイムアウトをオーバーライドするには、次のように setHint() を呼び出します。

        q.setHint("javax.persistence.query.timeout", 3000);

キーでエンティティを取得する場合は、これらのオプションの構成をオーバーライドすることはできません。

データクラスの拡張

JPA の DataNucleus 実装では構築プロセスにおいてコンパイル後の「拡張」手順を使用し、データクラスと JPA 実装を関連付けます。

次に示すコマンドを使用して、コンパイルしたクラスに対する拡張手順をコマンドラインから実行できます。

java -cp classpath org.datanucleus.enhancer.DataNucleusEnhancer
class-files

classpath には、appengine-java-sdk/lib/tools/ ディレクトリの JAR datanucleus-core-*.jardatanucleus-jpa-*datanucleus-enhancer-*.jarasm-*.jargeronimo-jpa-*.jar(ここで * は各 JAR の適切なバージョン番号です)とデータクラスのすべてが含まれている必要があります。

DataNucleus のバイトコード エンハンサの詳細については、DataNucleus のドキュメントを参照してください。

EntityManager インスタンスを取得する

アプリは、EntityManager クラスのインスタンスを使用して JPA を操作します。このインスタンスを取得するには、EntityManagerFactory クラスのインスタンスでメソッドをインスタンス化して呼び出します。ファクトリは JPA 構成("transactions-optional" という名前で識別)を使用して EntityManager インスタンスを作成します。

EntityManagerFactory インスタンスは初期化に時間がかかるため、単一のインスタンスをできるだけ再利用することをおすすめします。簡単なのは、次に示すようにシングルトン ラッパー クラスを静的なインスタンスで作成する方法です。

EMF.java

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public final class EMF {
    private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");

    private EMF() {}

    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

ヒント: "transactions-optional" は、persistence.xml ファイル内の構成セットの名前を参照します。アプリで複数の構成セットを使用する場合は、このコードを拡張して、必要に応じて Persistence.createEntityManagerFactory() を呼び出すことをおすすめします。このコードにより、各 EntityManagerFactory のシングルトン インスタンスがキャッシュに保存されます。

アプリはファクトリ インスタンスを使用して、データストアにアクセスするリクエストごとに 1 つの EntityManager インスタンスを作成します。

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;

import EMF;

// ...
    EntityManager em = EMF.get().createEntityManager();

EntityManager を使用して、データ オブジェクトを格納、更新、削除し、データストア クエリを実行します。

EntityManager インスタンスの使用が終了したら、その close() メソッドを呼び出す必要があります。EntityManager インスタンスをその close() メソッドの呼び出し後に使用するとエラーが発生します。

    try {
        // ... do stuff with em ...
    } finally {
        em.close();
    }

クラスとフィールドのアノテーション

JPA で保存した各オブジェクトが App Engine データストアのエンティティになります。エンティティの種類は、クラスの簡単な名前(パッケージ名を除いたもの)から派生します。クラスの各永続化フィールドがエンティティのプロパティを表しており、プロパティの名前はフィールドの名前と同じ(大文字と小文字もそのまま)です。

データストアでの Java クラスの格納および取得に JPA を使用できるように宣言するには、そのクラスに @Entity アノテーションを指定します。次に例を示します。

import javax.persistence.Entity;

@Entity
public class Employee {
    // ...
}

データストアに格納するデータクラスのフィールドは、デフォルトで永続的な型にするか、永続化を明示的に宣言する必要があります。DataNucleus のウェブサイトに、JPA のデフォルトの永続化動作の詳細を記載した表があります。フィールドの永続化を明示的に宣言するには、@Basic アノテーションを設定します。

import java.util.Date;
import javax.persistence.Enumerated;

import com.google.appengine.api.datastore.ShortBlob;

// ...
    @Basic
    private ShortBlob data;

次のフィールドの型を使用できます。

  • データストアでサポートされる主な型
  • コレクション(java.util.List<...> など)または主なデータストア型の値
  • @Entity クラスのインスタンス、またはインスタンスのコレクション
  • 埋め込みクラス(エンティティのプロパティとして格納)

データクラスには、パブリックまたは保護されたデフォルトのコンストラクタと、対応するデータストア エンティティの主キーを格納するための専用のフィールドが 1 つ必要です。このフィールドは、4 種類のキーフィールドから選択でき、種類ごとに異なる値の型とアノテーションを使用します(詳細については、データの作成: キーをご覧ください)。最も簡単なキーフィールドは長整数型の値です。この場合、オブジェクトがデータストアに初めて保存されるときに、クラスの他のインスタンス全体で一意の値が JPA によって自動的に設定されます。長整数型のキーでは、@Id アノテーションと @GeneratedValue(strategy = GenerationType.IDENTITY) アノテーションを使用します。

import com.google.appengine.api.datastore.Key;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

// ...
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

データクラスの例を次に示します。

import com.google.appengine.api.datastore.Key;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String firstName;

    private String lastName;

    private Date hireDate;

    // Accessors for the fields. JPA doesn't use these, but your application
    does.

    public Key getKey() {
        return key;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getHireDate() {
        return hireDate;
    }
    public void setHireDate(Date hireDate) {
        this.hireDate = hireDate;
    }
}

継承

JPA では、継承を使用するデータクラスを作成できます。ここでは JPA 継承が App Engine でどのように動作するかを説明しますが、その前にこのトピックに関する DataNucleus のドキュメントをお読みになることをおすすめします。ただし、注意が必要です。App Engine での JPA 継承は、DataNucleus のドキュメントに記載されているとおりに動作しますが、制限がいくつ設けられています。ここでは、この制限について説明したうえで、具体的な例をいくつか挙げていきます。

「JOINED」(結合)の継承方法では、1 つのデータ オブジェクトのデータを複数の「テーブル」に分割することが可能ですが、App Engine データストアでは結合がサポートされていません。このため、この継承方法でデータ オブジェクトを操作するには、継承のレベルごとにリモート プロシージャ コールが必要になります。しかし、これは実に非効率的なので、「JOINED」の継承方法はデータクラスではサポートされていません。

次に、「SINGLE_TABLE」(単一テーブル)の継承方法では、データ オブジェクトのデータを、継承階層のルートにある永続化クラスに関連付けられている 1 つの「テーブル」に格納できます。この方法は非効率的ではありませんが、現在サポートされていません。この方法については、今後のリリースで検討していきます。

「TABLE_PER_CLASS」(クラスごとのテーブル)と「MAPPED_SUPERCLASS」(マップされたスーパークラス)の方法は、DataNucleus のドキュメントに記載されているとおりに動作します。例を見てみましょう。

Worker.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@Entity
@MappedSuperclass
public abstract class Worker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String department;
}

Employee.java

// ... imports ...

@Entity
public class Employee extends Worker {
    private int salary;
}

Intern.java

import java.util.Date;
// ... imports ...

@Entity
public class Intern extends Worker {
    private Date internshipEndDate;
}

この例では、@MappedSuperclass アノテーションを Worker クラス宣言に追加しました。これにより、JPA で Worker のすべての永続化フィールドが、そのサブクラスのデータストア エンティティに格納されます。Employee インスタンスを指定して persist() を呼び出すことで作成されたデータストア エンティティには、「department」と「salary」の 2 つのプロパティがあります。Intern インスタンスを指定して persist() を呼び出すことで作成されたデータストア エンティティには、「department」と「internshipEndDate」の 2 つのプロパティがあります。データストアには種類「Worker」のエンティティは含まれません。

ここで、もう少し面白いことをしてみましょう。たとえば、EmployeeIntern の他に、退職した従業員を記述する Employee を特別に設定する必要があるとします。

FormerEmployee.java

import java.util.Date;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
// ... imports ...

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class FormerEmployee extends Employee {
    private Date lastDay;
}

この例では、@Inheritance アノテーションを FormerEmployee クラス宣言に追加し、strategy 属性を InheritanceType.TABLE_PER_CLASS に設定しました。これにより、JPA で FormerEmployee のすべての永続化フィールドとそのスーパークラスが、FormerEmployee インスタンスに対応するデータストア エンティティに格納されます。FormerEmployee インスタンスを指定して persist() を呼び出すことで作成されたデータストア エンティティには、「department」、「salary」、「lastDay」の 3 つのプロパティがあります。FormerEmployee に対応する種類「Employee」のエンティティはありませんが、ランタイム型が Employee のオブジェクトを指定して persist() を呼び出すと、種類「Employee」のエンティティが作成されます。

関係フィールドの宣言された型が、これらのフィールドに割り当てたオブジェクトのランタイム型と一致している場合は、関係と継承を組み合わせて利用できます。詳細については、ポリモーフィック関係のセクションをご覧ください。このセクションには JDO の例が示されていますが、概念と制限事項は JPA と同じです。

サポートされていない JPA 1.0 の機能

JPA インターフェースの次の機能は、App Engine でサポートされていません。

  • 多対多の所有関係および非所有関係。非所有関係は明示的なキー値を使用して実装できますが、API では型のチェックは行われません。
  • 「結合」クエリ。親の種類に対してクエリを実行する場合は、フィルタで子エンティティのフィールドを使用することはできません。キーを使用して、親の関係フィールドを直接クエリでテストすることはできます。
  • 集約クエリ(group by、having、sum、avg、max、min)。
  • ポリモーフィック クエリ。クラスのクエリを実行して、サブクラスのインスタンスを取得することはできません。各クラスは、データストア内の個別のエンティティの種類として表されます。