JDO を使用したデータクラスの定義

JDO を使用すると、簡単な Java データ オブジェクト(「POJO(Plain Old Java Object)」と呼ばれることもあります)をデータストアに格納できます。PersistenceManager で永続化した各オブジェクトが、データストアのエンティティになります。データクラスのインスタンスの格納と再構築の方法を指定するには、アノテーションを使用します。

注: 以前のバージョンの JDO では、Java アノテーションではなく .jdo XML ファイルを使用します。このファイルは JDO 2.3 でも引き続き動作しますが、ここでは、データクラスで Java アノテーションを使用する方法についてのみ説明します。

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

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

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

import javax.jdo.annotations.PersistenceCapable;

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

データストアに格納するデータクラスのフィールドは、永続化フィールドとして宣言する必要があります。フィールドの永続化を宣言するには、そのフィールドに @Persistent アノテーションを指定します。

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

フィールドを永続化しないことを宣言する(この場合、データストアに格納されず、オブジェクトの取得時に復元されません)には、そのフィールドに @NotPersistent アノテーションを指定します。

ヒント: @Persistent アノテーションも @NotPersistent アノテーションも指定されていない場合、JDO では、特定の型のフィールドについてはデフォルトで永続化が指定されますが、それ以外の型のフィールドはすべてデフォルトでは永続化されません。この動作の詳細については、DataNucleus のドキュメントをご覧ください。App Engine データストアの主な値の型すべてが、JDO 仕様においてデフォルトで永続化されるわけではありません。したがって、永続化フィールドか非永続化フィールドかを明確にするために、明示的に @Persistent または @NotPersistent アノテーションを指定することをおすすめします。

使用できるフィールドの型は次のとおりです。詳細については、後で説明します。

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

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

ヒント: すべての永続化フィールドを private または protected(または package protected)に設定し、アクセサ メソッドによるパブリック アクセスのみを提供します。他のクラスから永続化フィールドに直接アクセスすると、JDO クラスの拡張が行われない場合があります。他のクラスを @PersistenceAware にする方法もあります。詳細については、DataNucleus ドキュメントをご覧ください。

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

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PrimaryKey;

// ...
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

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

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

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String firstName;

    @Persistent
    private String lastName;

    @Persistent
    private Date hireDate;

    public Employee(String firstName, String lastName, Date hireDate) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.hireDate = hireDate;
    }

    // Accessors for the fields. JDO 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;
    }
}

主な値の型

主な型の値を 1 つ保持するプロパティを表すには、Java 型のフィールドを宣言し、@Persistent アノテーションを使用します。

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

シリアル化可能なオブジェクト

フィールドの値には、シリアル化可能なクラスのインスタンスを指定できます。インスタンスのシリアル化した値が BLOB 型の 1 つのプロパティ値として格納されます。JDO で値をシリアル化するには、そのフィールドでアノテーション @Persistent(serialized=true) を使用します。BLOB 値はインデックスに登録されないため、クエリのフィルタや並べ替え順序に使用することはできません。

ファイル(ファイルの内容、ファイル名、MIME タイプなど)を表す簡単なシリアル化可能なクラスの例を次に示します。これは JDO データクラスではないので、永続化アノテーションはありません。

import java.io.Serializable;

public class DownloadableFile implements Serializable {
    private byte[] content;
    private String filename;
    private String mimeType;

    // ... accessors ...
}

シリアル化可能なクラスのインスタンスを BLOB 値としてプロパティに格納するには、そのクラスを型とするフィールドを宣言し、@Persistent(serialized = "true") アノテーションを使用します。

import javax.jdo.annotations.Persistent;
import DownloadableFile;

// ...
    @Persistent(serialized = "true")
    private DownloadableFile file;

子オブジェクトと関係

フィールド値が @PersistenceCapable クラスのインスタンスの場合、2 つのオブジェクト間に 1 対 1 の所有関係が作成されます。フィールドにこれらの参照のコレクションが指定されている場合は、1 対多の所有関係が作成されます。

重要: 所有関係は、トランザクション、エンティティ グループ、カスケード削除に影響します。詳細については、トランザクション関係をご覧ください。

Employee オブジェクトと ContactInfo オブジェクト間の 1 対 1 の所有関係の簡単な例を次に示します。

ContactInfo.java

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

@PersistenceCapable
public class ContactInfo {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String streetAddress;

    @Persistent
    private String city;

    @Persistent
    private String stateOrProvince;

    @Persistent
    private String zipCode;

    // ... accessors ...
}

Employee.java

import ContactInfo;
// ... imports ...

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private ContactInfo myContactInfo;

    // ... accessors ...
}

この例では、アプリケーションで Employee インスタンスを作成し、その myContactInfo フィールドに新しい ContactInfo インスタンスを設定した後、Employee インスタンスを pm.makePersistent(...) で保存すると、データストアで 2 つのエンティティが作成されます。1 つは種類 "ContactInfo" で、ContactInfo インスタンスを表します。もう 1 つは種類 "Employee" です。ContactInfo エンティティのキーには、そのエンティティ グループの親として Employee エンティティのキーが含まれています。

埋め込みクラス

埋め込みクラスを使用すると、新しいデータストア エンティティを作成して関係を形成しなくても、クラスを使用してフィールド値をモデル化できます。オブジェクト値のフィールドは、保持されているオブジェクトのデータストア エンティティに直接格納されます。

すべての @PersistenceCapable データクラスを、他のデータクラスの埋め込みオブジェクトとして使用できます。クラスの @Persistent フィールドは、オブジェクトに埋め込まれます。埋め込み対象のクラスに @EmbeddedOnly アノテーションを指定すると、そのクラスは埋め込みクラスとしてのみ使用できます。埋め込みクラスは個別のエンティティとして格納されないので、主キーフィールドは必要ありません。

埋め込みクラスの例を次に示します。この例では、埋め込みクラスを、そのクラスを使用するデータクラスの内部クラスにしています。これはクラスを埋め込み可能にするのに便利な方法ですが、必須というわけではありません。

import javax.jdo.annotations.Embedded;
import javax.jdo.annotations.EmbeddedOnly;
// ... imports ...

@PersistenceCapable
public class EmployeeContacts {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    Key key;
    @PersistenceCapable
    @EmbeddedOnly
    public static class ContactInfo {
        @Persistent
        private String streetAddress;

        @Persistent
        private String city;

        @Persistent
        private String stateOrProvince;

        @Persistent
        private String zipCode;

        // ... accessors ...
    }

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;
}

埋め込みクラスのフィールドは、各フィールドの名前と対応するプロパティの名前を使用して、エンティティのプロパティとして格納されます。オブジェクトに埋め込みクラス型のフィールドが複数ある場合は、競合しないようにフィールドの名前を変更します。新しいフィールド名を指定するには、@Embedded アノテーションの引数を使用します。例:

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;

    @Persistent
    @Embedded(members = {
        @Persistent(name="streetAddress", columns=@Column(name="workStreetAddress")),
        @Persistent(name="city", columns=@Column(name="workCity")),
        @Persistent(name="stateOrProvince", columns=@Column(name="workStateOrProvince")),
        @Persistent(name="zipCode", columns=@Column(name="workZipCode")),
    })
    private ContactInfo workContactInfo;

同様に、オブジェクトのフィールド名が、埋め込みクラスのフィールド名と競合しないようにします(埋め込みフィールドの名前を変更しない場合)。

埋め込みクラスの永続化プロパティは、他のフィールドと同じエンティティに格納されるので、埋め込みクラスの永続化フィールドは、JDOQL クエリのフィルタや並べ替え順序に使用できます。埋め込みフィールドを参照するには、外部フィールドの名前、ピリオド(.)、埋め込みフィールドの名前を使用します。これは、埋め込みフィールドのプロパティ名が @Column アノテーションを使用して変更されているかどうかにかかわらず機能します。

    select from EmployeeContacts where workContactInfo.zipCode == "98105"

コレクション

データストア プロパティには複数の値を指定できます。JDO では、これをコレクション型の 1 つのフィールドで表します。コレクションには、主な値の型のいずれかか、シリアル化可能なクラスを指定します。次のコレクション型がサポートされます。

  • java.util.ArrayList<...>
  • java.util.HashSet<...>
  • java.util.LinkedHashSet<...>
  • java.util.LinkedList<...>
  • java.util.List<...>
  • java.util.Map<...>
  • java.util.Set<...>
  • java.util.SortedSet<...>
  • java.util.Stack<...>
  • java.util.TreeSet<...>
  • java.util.Vector<...>

フィールドを List として宣言した場合、データストアによって返されるオブジェクトの値は ArrayList です。フィールドを Set として宣言した場合、データストアは HashSet を返します。フィールドを SortedSet として宣言した場合、データストアは TreeSet を返します。

たとえば、型が List<String> のフィールドは、プロパティの 0 個以上の文字列値として格納されます(文字列ごとに List の 1 つの値)。

import java.util.List;
// ... imports ...

// ...
    @Persistent
    List<String> favoriteFoods;

@PersistenceCapable クラスの子オブジェクトのコレクションの場合は、複数のエンティティが 1 対多の関係で作成されます。詳細については関係をご覧ください。

複数の値を持つデータストア プロパティは、クエリのフィルタ処理や並べ替え順序の動作が特殊です。詳細については、データストア クエリをご覧ください。

オブジェクトのフィールドとエンティティのプロパティ

App Engine データストアでは、指定されたプロパティを持たないエンティティと、そのプロパティに null 値が指定されているエンティティが区別されます。JDO ではこの区別がサポートされていないため、すべてのオブジェクト フィールドに値(null も可能)が設定されています。null を指定できる値の型(intboolean などの組み込みの型を除く)のフィールドが null に設定されている場合、オブジェクトを保存すると、そのエンティティのプロパティには null 値が設定されます。

オブジェクトに読み込まれたデータストア エンティティに、オブジェクトのいずれかのフィールドに対応するプロパティがなく、かつ、フィールドの型が 1 つの値を持つ、null を指定できる型の場合、そのフィールドは null に設定されます。オブジェクトをデータストアに保存し直すと、プロパティはデータストアでも null 値に設定されます。フィールドの型が null 値を指定できない型の場合、対応するプロパティを持たないエンティティを読み込むと例外がスローされます。この例外は、インスタンスを再作成したのと同じ JDO クラスからエンティティを作成した場合は発生しませんが、JDO クラスが異なる場合や、JDO ではなく低レベル API を使用してエンティティを作成した場合は発生します。

フィールドの型が主なデータ型またはシリアル化可能なクラスのコレクションで、エンティティのプロパティに値がない場合は、プロパティに 1 つの null 値を設定すると、データストアで空のコレクションを表します。フィールドの型が配列型の場合は、要素が 0 個の配列が割り当てられます。読み込んだオブジェクトのプロパティに値がない場合は、フィールドに該当する型の空のコレクションが割り当てられます。データストアの内部では、空のコレクションと、1 つの null 値が含まれるコレクションは区別されます。

エンティティのプロパティに対応するフィールドがオブジェクトにない場合、オブジェクトからはそのプロパティにアクセスできません。オブジェクトをデータストアに保存し直すと、その余分なプロパティは削除されます。

JDO では、エンティティのプロパティの値の型と、それに対応するオブジェクトのフィールドの型が異なると、そのフィールドの型への値のキャストを試みます。値をフィールドの型にキャストできないと、ClassCastException がスローされます。数値(長整数および倍精度浮動小数点数)の場合、値のキャストではなく変換が行われます。数値のプロパティ値がフィールドの型よりも大きい場合、変換は例外をスローせずにオーバーフローします。

次の行を追加して、インデックス付けされていないプロパティを宣言できます。

    @Extension(vendorName="datanucleus", key="gae.unindexed", value="true")

このプロパティは、クラス定義に配置されています。インデックス付けされていないプロパティの詳細については、メイン ドキュメントのインデックス付けされていないプロパティ セクションをご覧ください。

継承

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

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

2 つ目として、「superclass-table」(スーパークラス テーブル)の継承方法では、データ オブジェクトのデータをスーパークラスの「テーブル」に格納できます。この方法は非効率的ではありませんが、現在サポートされていません。この方法については、今後のリリースで検討していきます。

「subclass-table」(サブクラス テーブル)と「complete-table」(完全なテーブル)の方法は、DataNucleus のドキュメントに記載されているとおりに動作します。また、継承階層のルートにあるすべてのデータ オブジェクトで「新しいテーブル」を使用できます。例を見てみましょう。

Worker.java

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Inheritance;
import javax.jdo.annotations.InheritanceStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)
public abstract class Worker {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String department;
}

Employee.java

// ... imports ...

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

Intern.java

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

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

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

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

FormerEmployee.java

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

@PersistenceCapable
@Inheritance(customStrategy = "complete-table")
public class FormerEmployee extends Employee {
    @Persistent
    private Date lastDay;
}

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

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