お気に入り | 日本語 | ログイン

関係

オブジェクト タイプのフィールドを使用し、永続オブジェクト同士を関連付けることができます。永続オブジェクト間の関係は、所有、または非所有のいずれかとなります。所有では、オブジェクトの一方は他方がなくしては存在できませんが、非所有では、どちらのオブジェクトもお互いの関係から独立して存在することができます。App Engine での JDO インターフェースの実装は、1 対 1 の所有関係および 1 対多の所有関係を、一方向または双方向の両方でモデリングできます。非所有関係は自然構文では現在サポートされていませんが、フィールドにデータストア キーを直接保存することで関係を管理できます。App Engine はエンティティ グループ内に関連するエンティティを自動的に作成し、関連したオブジェクトの一括更新をサポートしています。しかし、データストア トランザクションをいつ使用するかはアプリケーション側に一任されます。

1 対 1 の所有関係

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

次に、ContactInfo データ クラスと Employee データ クラスで、Employee から ContactInfo への 1 対 1 の関係を定義する方法を示します。

ContactInfo.java

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

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

    @Persistent
    private String streetAddress;

    // ...
}

Employee.java

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

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private ContactInfo contactInfo;

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

    // ...
}

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

子のクラスには、親キーの情報を保存できるタイプのキー フィールド(キーや文字列として表されたキー値など)が必要です。キー フィールドのタイプの詳細については、データの作成: キーをご覧ください。

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

ContactInfo.java

import Employee;

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

子オブジェクトは、最初にアクセスされたときにデータストアから読み込まれます。親オブジェクトで子オブジェクトにアクセスしない場合、子オブジェクトのエンティティは読み込まれません(データストア インターフェースは、子オブジェクトの「積極的な」読み込みはサポートしていません。データストアはクエリの結合はサポートしていないため、積極的な読み込みを実装しても、アプリケーションからデータストアへのコール数を減らすことはできません)。

1 対多の所有関係

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

Employee.java

import java.util.List;

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

双方向の 1 対多関係は、親クラスのフィールドがアノテーション @Persistent(mappedBy = "...") を使用し、その値が子クラスのフィールド名であるという点では 1 対 1 関係に似ています。

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 への拡張であるアノテーションを使用し、要素のプロパティに基づいて順序を指定できます。

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 List<ContactInfo>();

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

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

効率的に実行するには、順序付けされた 1 対多関係では、できる限り順序句を明示的に使用してください。

非所有関係

JDO API は、所有関係に加え、非所有関係を管理するための機能も提供しています。JDO の App Engine の実装はまだこの機能を実装していませんが、モデル オブジェクトのインスタンス(またはインスタンスのコレクション)の代わりに Key を使用しこれらの関係を管理できます。キー オブジェクトの保存は、2 つのオブジェクト間で任意の「外部キー」をモデリングすることだと考えることができます。データストアは、これらのキー参照の一貫性を保証していません。しかし、キーを使用することで、2 つのオブジェクト間のすべての関係のモデリング(およびフェッチ)を非常に簡単に実行できます。しかし、この方法を使用する場合、いくつか注意点があります。まず、キーが正しいタイプであることをアプリケーション側で管理しなければなりません。JDO およびコンパイラはタイプのチェックは実行しません。次に、関係の両側のオブジェクトのアトミックな更新を実行するには、すべてのオブジェクトが同じエンティティ グループに属している必要があります。

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

1 対 1 の非所有関係

人物と食べ物のモデリングを行うとします。人物には 1 つの好物しか設定できません。また、複数の人が同じ食べ物を好物としていることが考えられるため、好物は人物には属していません。

Person.java

// ... imports ...

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private Key favoriteFood;

    // ...
}

Food.java

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

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Food {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    // ...
}

この例では、Person にタイプが Food のメンバーを割り当てて人物の好物を表す代わりに、Person にタイプが Key のメンバーを割り当てます。ここで、KeyFood オブジェクトの一意な識別子となります。Person.favoriteFood で参照される Person のインスタンスと Food のインスタンスが同じエンティティ グループに含まれていない限り、人物と、その人物の好物を 1 つのトランザクションでは更新できません。

1 対多の非所有関係

人物に複数の好物を割り当てることができるとします。1 対 1 の場合と同様に、複数の人が同じ食べ物を好物としていることが考えられるため、好物は人物には属していません。

Person.java

// ... imports ...

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

この例では、Person にタイプが Set<Food> のメンバーを割り当てて人物の好物を表す代わりに、Person にタイプが Set<Key> のメンバーを割り当てます。ここで、このセットにはオブジェクトの一意な識別子である Food が含まれます。Person に含まれる Food のインスタンスと Person.favoriteFoods のインスタンスが同じエンティティ グループに含まれていない限り、人物と、その人物の好物を 1 つのトランザクションでは更新できません。

多対多関係

関係の両側でキーのコレクションを保持することで、1 対多関係をモデリングできます。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 値のセットを保持し、FoodPerson オブジェクトを好物に選んだ人を位置に識別する 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 に含まれる Food のインスタンスと Person.favoriteFoods のインスタンスが同じエンティティ グループに含まれていない限り、人物と、その人物の好物を 1 つのトランザクションでは更新できません。オブジェクトが同じエンティティ グループに属するように設定するのは適切ではありません。そのため、アプリケーションはある人物の好物を更新する際、その食べ物を好物としている人物のグループが更新されない場合、逆に、その食べ物を好物とする人物のグループが更新されても、そのグループに属する人の好物のグループが更新されない場合があることを想定しなければなりません。

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

所有関係にあるオブジェクトがデータストアに保存されると、そのオブジェクトと関係があるすべてのオブジェクトの内、保存する必要のあるもの(新しいオブジェクト、または最後に読み込まれてから変更されたもの)は自動的に保存されます。これがトランザクションとエンティティ グループに対して影響する点について言及しておく必要があります。

上記の 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 FatalUserException が発生します。

子オブジェクトが変更された親オブジェクトを保存すると、子オブジェクトへの変更も保存されます。このように、関連するすべての子オブジェクトに対して親オブジェクトを永続とし、変更を保存する場合はトランザクションを使用するのが良い方法であるといえます。

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

App Engine の JDO の実装により、すべての所有関係は「依存」します。つまり親オブジェクトが削除されると、子オブジェクトも削除されます。親の依存先フィールドに新しい値を割り当てることで所有関係が消滅すると、古い子オブジェクトも削除されます。

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

注意: JDO 実装は、データストアではなく、依存する子オブジェクトを削除します。低レベルの API または管理コンソールから親オブジェクトを削除すると、関連する子オブジェクトは削除されません。