JDO でのエンティティの関係

オブジェクト型のフィールドを使用して、永続化オブジェクト間の関係をモデル化できます。永続化オブジェクト間の関係は、「所有」または「非所有」です。「所有」では一方のオブジェクトがないと他方は存在できませんが、「非所有」ではどちらのオブジェクトも互いの関係にかかわらず独立して存在することができます。App Engine での JDO インターフェースの実装では、所有および非所有の 1 対 1 の関係および 1 対多の関係を、一方向と双方向の両方でモデル化できます。

非所有関係は、App Engine DataNucleus プラグインのバージョン 1.0 ではサポートされていませんが、フィールドにデータストア キーを直接格納することで関係を管理できます。App Engine では、エンティティ グループ内に関連エンティティが自動的に作成され、関連するオブジェクトを一緒に更新できるようになりますが、データストア トランザクションをいつ使用するかはアプリ側で決定されます。

App Engine DataNucleus プラグインのバージョン 2.x では、通常の構文での非所有関係がサポートされます。非所有関係のセクションでは、非所有関係を作成する方法をプラグインのバージョンごとに説明します。App Engine DataNucleus プラグインのバージョン 2.x にアップグレードする方法については、DataNucleus プラグインのバージョン 2.x への移行をご覧ください。

1 対 1 の所有関係

2 つの永続化オブジェクト間に一方向の 1 対 1 の所有関係を設定するには、関連クラスのクラスを型として持つフィールドを使用します。

次の例では、ContactInfo データクラスと Employee データクラスが定義され、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;

    // ...
}

Employee.java

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

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

    @Persistent
    private ContactInfo contactInfo;

    ContactInfo getContactInfo() {
        return contactInfo;
    }
    void setContactInfo(ContactInfo contactInfo) {
        this.contactInfo = contactInfo;
    }

    // ...
}

永続化オブジェクトは、データストアでは異なる 2 つの種類の別個のエンティティとして表されます。関係は、エンティティ グループの関係を使用して表されます。つまり、子のキーは親のキーをエンティティ グループの親として使用します。アプリで親オブジェクトのフィールドを使用して子オブジェクトにアクセスすると、JDO 実装はエンティティ グループの親へのクエリを実行して子を取得します。

子クラスには、親キーの情報を格納できる型のキーフィールド(キーか、文字列としてエンコードされたキー値のいずれか)が必要です。キーフィールドの型の情報については、データの作成: キーをご覧ください。

両方のクラスのフィールドを使用して双方向の 1 対 1 の関係を作成し、子クラスのフィールドのアノテーションを使用して、フィールドが双方向の関係を表すことを宣言します。子クラスのフィールドには、引数 mappedBy = "..." 付きの @Persistent アノテーションが必要です。ここで、その値は、親クラスのフィールドの名前になります。一方のオブジェクトのフィールドに値が設定されると、もう一方のオブジェクトの対応する参照フィールドにも自動的に値が設定されます。

ContactInfo.java

import Employee;

// ...
    @Persistent(mappedBy = "contactInfo")
    private Employee employee;

子オブジェクトは、最初にアクセスされたときにデータストアから読み込まれます。親オブジェクトから子オブジェクトにアクセスしない場合、子オブジェクトのエンティティは読み込まれません。子を読み込むには、PersistenceManager を閉じる前にその子に「touch」するか(上記の例では getContactInfo() の呼び出しでこれを行っています)、子フィールドをデフォルトの取得グループに明示的に追加して、親と一緒に子の取得と読み込みが行われるようにします。

Employee.java

import ContactInfo;

// ...
    @Persistent(defaultFetchGroup = "true")
    private ContactInfo contactInfo;

1 対多の所有関係

あるクラスのオブジェクトから別のクラスの複数のオブジェクトへの 1 対多の関係を作成するには、関連するクラスのコレクションを使用します。

Employee.java

import java.util.List;

// ...
    @Persistent
    private List<ContactInfo> contactInfoSets;

1 対多の双方向の関係は 1 対 1 の関係に似ており、親クラスのフィールドでアノテーション @Persistent(mappedBy = "...") を指定する必要があります。ここで、その値は、子クラスのフィールド名になります。

Employee.java

import java.util.List;

// ...
    @Persistent(mappedBy = "employee")
    private List<ContactInfo> contactInfoSets;

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

データクラスの定義: コレクションで紹介されているコレクション型では、1 対多の関係がサポートされています。ただし、配列で 1 対多の関係はサポートされていません

App Engine ではクエリの結合はサポートされていないため、子エンティティの属性を使用して親エンティティにクエリを実行することはできません(埋め込みクラスには親エンティティのプロパティが格納されているため、埋め込みクラスのプロパティへのクエリは実行できます。データクラスの定義: 埋め込みクラスをご覧ください)。

コレクションの並べ替え順序を保持する方法

順序付きコレクション(List<...> など)では、親オブジェクトを保存する際、オブジェクトの並べ替え順序が保持されます。JDO では、データベースで各オブジェクトの位置をオブジェクトのプロパティとして格納することにより、この順序を保持する必要があります。App Engine では、この順序を対応するエンティティのプロパティとして格納し、プロパティ名は、親のフィールド名の後に _INTEGER_IDX を付けたものになります。位置のプロパティは効率的ではありません。コレクションで要素を追加、削除、移動すると、コレクション内の変更箇所より後のエンティティもすべて更新する必要が生じます。この処理は時間がかかり、トランザクションで実行されない場合、エラーが発生しやすくなります。

コレクション内の任意の順序を保持する必要はないが順序付きコレクション型を使用する必要がある場合は、アノテーションにより、要素のプロパティに基づく順序付けを指定できます。これは、DataNucleus が提供する JDO 拡張の 1 つです。

import java.util.List;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    @Order(extensions = @Extension(vendorName="datanucleus",key="list-ordering", value="state asc, city asc"))
    private List<ContactInfo> contactInfoSets = new ArrayList<ContactInfo>();

@Order アノテーション(list-ordering 拡張を使用)では、コレクションの要素の並べ替え順序を JDOQL の順序指定句として指定します。並べ替えには要素のプロパティの値を使用します。クエリと同様、コレクションの全要素に、順序指定句で使用したプロパティの値が含まれている必要があります。

コレクションにアクセスすると、クエリが実行されます。フィールドの順序指定句で複数の並べ替え順序を使用する場合、クエリを実行するにはデータストア インデックスが必要です。インデックスの詳細については、データストア インデックスのページをご覧ください。

効率的にするために、順序が指定されたコレクション型の 1 対多の関係では、できる限り順序指定句を明示的に使用してください。

非所有関係

JDO API には、所有関係のほか、非所有関係を管理するための機能も用意されています。この機能の動作は、使用している App Engine DataNucleus プラグインのバージョンによって異なります。

  • DataNucleus プラグインのバージョン 1 では、通常の構文を使用した非所有関係は実装されませんが、モデル オブジェクトのインスタンス(またはインスタンスのコレクション)の代わりに Key 値を使用して、こうした関係を管理できます。キー オブジェクトの格納は、2 つのオブジェクト間で任意の「外部キー」をモデル化することだと考えることができます。データストアでは、これらの Key 参照の一貫性が保証されていませんが、Key を使用することで、2 つのオブジェクト間のあらゆる関係をとても簡単にモデル化(および取得)できます。

    ただし、この方法を使用する場合は、キーが適切な種類であることを確認しなければなりません。Key の型は、JDO やコンパイラではチェックされません。
  • DataNucleus プラグインのバージョン 2.x には、通常の構文を使用した非所有関係が実装されています。

ヒント: 場合によっては、所有関係を非所有関係のようにモデル化しなければならないことがあります。所有関係に含まれるすべてのオブジェクトは自動的に同じエンティティ グループに属しますが、エンティティ グループでは 1 秒間に 1~10 件の書き込みにしか対応できないからです。このため、たとえば親オブジェクトが毎秒 0.75 件の書き込みを受信し、子オブジェクトが毎秒 0.75 件の書き込みを受信する場合、この関係を非所有関係としてモデル化し、親と子をそれぞれ独立した別個のエンティティ グループに配置する方が有効である場合があります。

1 対 1 の非所有関係

人と食べ物のモデル化を行うとします。1 人に 1 つの好物しか設定できない場合、複数の人が同じ食べ物を好物としていることが考えられるため、好物は人には属しません。ここでは、このような関係のモデル化について説明します。

JDO 2.3 の場合

この例では、Person に型 Key のメンバーを割り当てます。ここで、Key は、Food オブジェクト固有の識別子になります。Person のインスタンスと、Person.favoriteFood によって参照される Food のインスタンスが同じエンティティ グループに属していない場合、JDO 構成でクロスグループ(XG)トランザクションの有効化を設定しない限り、その人とその人の好物は 1 つのトランザクションで更新できません。

Person.java

// ... imports ...

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

    @Persistent
    private Key favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

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

    // ...
}

JDO 3.0 の場合

この例では、Person に好物を表すキーを割り当てるのではなく、型 Food のプライベート メンバーを作成します。

Person.java

// ... imports ...

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

    @Persistent
    @Unowned
    private Food favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

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

    // ...
}

1 対多の非所有関係

次は、人に複数の好物を設定できるようにします。ここでもやはり複数の人が同じ食べ物を好物としていることが考えられるので、好物は人に属しません。

JDO 2.3 の場合

この例では、人の好みの食べものを表すために Person に型 Set<Food> のメンバーを渡すのではなく、Person に型 Set<Key> のメンバーを渡します。ここで、そのセットには、Food オブジェクト固有の識別子が含まれます。なお、Person のインスタンスと、Person.favoriteFoods に含まれる Food のインスタンスが同じエンティティ グループに属していない場合、それらを同じトランザクションで更新するには、JDO 構成でクロスグループ(XG)トランザクションの有効化を設定する必要があります。

Person.java

// ... imports ...

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

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

JDO 3.0 の場合

この例では、Person に型 Set<Food> のメンバーを割り当てます。ここで、そのセットは、Person の好物を表します。

Person.java

// ... imports ...

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

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

多対多の関係

関係の双方でキーのコレクションを保持することで、多対多の関係をモデル化できます。上記の例を変更して、Food で、その食べものを好物に選んだ人を管理できるようにします。

Person.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> favoriteFoods;

Food.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> foodFans;

この例では、Person にはその人の好物である Food オブジェクトを一意に識別する Key 値のセットが保持され、Food にはその食べものを好物に選んだ Person オブジェクトを一意に識別する Key 値のセットが保持されます。

Key 値を使用して多対多の関係をモデル化する場合、関係の双方を管理するのはアプリの責務という点に注意してください。

Album.java

// ...
public void addFavoriteFood(Food food) {
    favoriteFoods.add(food.getKey());
    food.getFoodFans().add(getKey());
}

public void removeFavoriteFood(Food food) {
    favoriteFoods.remove(food.getKey());
    food.getFoodFans().remove(getKey());
}

Person のインスタンスと、Person.favoriteFoods に含まれる Food のインスタンスが同じエンティティ グループに属していない場合、それらを同じトランザクションで更新するには、JDO 構成でクロスグループ(XG)トランザクションの有効化を設定する必要があります。

関係、エンティティ グループ、トランザクション

所有関係が設定されているオブジェクトをアプリケーションでデータストアに保存すると、そのオブジェクトとの関係を通じて参照される可能性があり、保存する必要があるすべてのオブジェクト(新しいものや、最後に読み込まれた後で変更されたもの)が自動的に保存されます。このことは、トランザクションとエンティティ グループにとって重要な意味を持ちます。

上記の Employee クラスと ContactInfo クラスの間に単方向の関係を使用する、次の例を考えます。

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    pm.makePersistent(e);

新しい Employee オブジェクトを pm.makePersistent() メソッドを使用して保存すると、関連する新しい ContactInfo オブジェクトが自動的に保存されます。どちらのオブジェクトも新しいため、App Engine は Employee エンティティを ContactInfo エンティティの親として使用して、同じエンティティ グループに 2 つの新しいエンティティを作成します。同様に、Employee オブジェクトがすでに保存されていて、関連する ContactInfo オブジェクトが新しい場合は、App Engine により、既存の Employee エンティティを親として、ContactInfo エンティティが作成されます。

ただし、この例の pm.makePersistent() の呼び出しでトランザクションは使用されません。明示的なトランザクションを使用していない場合、どちらのエンティティも個別のアトミックな処理を使用して作成されます。この場合、Employee エンティティの作成が成功しても、ContactInfo エンティティの作成が失敗する可能性もあります。確実に両方のエンティティを作成するには(またはどちらのエンティティも作成されないようにするには)、トランザクションを使用する必要があります。

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    try {
        Transaction tx = pm.currentTransaction();
        tx.begin();
        pm.makePersistent(e);
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

関係が確立される前に両方のオブジェクトを保存した場合、App Engine は、既存の ContactInfo エンティティを Employee エンティティのエンティティ グループに「移動」できません。エンティティ グループを割り当てることができるのは、エンティティの作成時のみであるためです。App Engine では参照を使用して関係を確立できますが、関連するエンティティは同じグループには含まれません。この場合、2 つのエンティティを同じトランザクションで更新または削除するには、JDO 構成でクロスグループ(XG)トランザクションの有効化を設定します。XG トランザクションを使用しない場合、異なるグループのエンティティを同じトランザクションで更新または削除しようとすると、JDOFatalUserException がスローされます。

子オブジェクトを変更して、その親オブジェクトを保存すると、子オブジェクトへの変更も保存されます。このように関連するすべての子オブジェクトの永続性を親オブジェクトが確保できるようにすること、また、変更を保存するときはトランザクションを使用できるようにすることをおすすめします。

依存する子とカスケード削除

所有関係は「依存」の関係に基づいています。つまり子は親なくしては存在できません。依存の関係では、親オブジェクトを削除すると、子オブジェクトもすべて削除されます。また、親の依存フィールドに新しい値を割り当てて依存の所有関係を破棄すると、古い子は削除されます。1 対 1 の所有関係は、子を参照する親オブジェクトのフィールドの Persistent アノテーションに dependent="true" を追加することで、依存関係となるように宣言できます。

// ...
    @Persistent(dependent = "true")
    private ContactInfo contactInfo;

1 対多の所有関係は、子コレクションを参照する親オブジェクトのフィールドに @Element(dependent = "true") アノテーションを追加することで、依存関係となるように宣言できます。

import javax.jdo.annotations.Element;
// ...
    @Persistent
    @Element(dependent = "true")
    private List contactInfos;

オブジェクトの作成や更新と同様に、カスケード削除におけるすべての削除が単一のアトミックな処理で実行されるようにするには、トランザクションで削除を実行する必要があります。

注: JDO 実装では、データストアではなく、依存する子オブジェクトが削除されます。低レベル API や Google Cloud Console を使用して親エンティティを削除した場合、関連する子オブジェクトは削除されません。

ポリモーフィック関係

JDO 仕様にはポリモーフィック関係のサポートが含まれていますが、App Engine の JDO 実装ではポリモーフィック関係はまだサポートされていません。サービスの今後のリリースでこの制約をなくすよう努めてまいります。1 つの一般的な基本クラスを使用して複数のオブジェクト型を参照する必要がある場合は、非所有関係を実装する際に使用した方法を利用し、キー参照を格納することをおすすめします。たとえば、AppetizerEntreeDessert の専門分野を含む基本クラス Recipe があり、Chef のお気に入りの Recipe をモデル化する場合は、次のようにしてモデル化できます。

Recipe.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 Recipe {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private int prepTime;
}

Appetizer.java

// ... imports ...

@PersistenceCapable
public class Appetizer extends Recipe {
// ... appetizer-specific fields
}

Entree.java

// ... imports ...

@PersistenceCapable
public class Entree extends Recipe {
// ... entree-specific fields
}

Dessert.java

// ... imports ...

@PersistenceCapable
public class Dessert extends Recipe {
// ... dessert-specific fields
}

Chef.java

// ... imports ...

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

    @Persistent(dependent = "true")
    private Recipe favoriteRecipe;
}

残念ながら、Entree をインスタンス化して Chef.favoriteRecipe に割り当てた場合、Chef オブジェクトを永続化しようとすると UnsupportedOperationException が発生します。これは、オブジェクトのランタイム型 Entree が、関係フィールドの宣言型 Recipe と一致しないことが原因です。これを回避するには、Chef.favoriteRecipe の型を Recipe から Key に変更します。

Chef.java

// ... imports ...

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

    @Persistent
    private Key favoriteRecipe;
}

Chef.favoriteRecipe は関係フィールドでなくなるため、あらゆる型のオブジェクトを参照できます。この方法の欠点は、非所有関係の場合と同様、この関係を手動で管理しなければならないことです。