JDO 中的实体关系

您可以使用对象类型的字段对持久化对象之间的关系进行建模。持久化对象之间的关系可以描述为“有主”关系(即,一个对象无法脱离另一个对象而存在)或“无主”关系(即,两个对象都可以独立存在,不受其彼此间关系的影响)。JDO 接口的 App Engine 实现可以对有主和无主的一对一关系以及一对多关系建模,包括单向和双向的关系。

1.0 版 App Engine DataNucleus 插件不支持无主关系,但您可以通过直接在字段中存储数据存储区键来自行管理这些关系。App Engine 会自动在实体组中创建相关实体,以支持同时更新相关对象,但应用必须知道何时使用数据存储区事务。

2.x 版 App Engine DataNucleus 插件支持使用自然语法的无主关系。无主关系部分介绍了如何在各插件版本中创建无主关系。要升级到 2.x 版 App Engine DataNucleus 插件,请参阅迁移到 2.x 版 App Engine DataNucleus 插件

有主的一对一关系

您可以使用特定类型的字段(其类型为相关类对应的类别),在两个持久化对象之间创建单向的一对一有主关系。

以下示例定义了一个 ContactInfo 数据类和一个 Employee 数据类,并将 Employee 与 ContactInfo 之间的关系定义为一对一关系。

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;
    }

    // ...
}

这些持久化对象在数据存储区中表示为属于两种不同种类的两个不同实体。二者之间的关系使用实体组关系表示:子级的键使用父级的键作为其实体组父项。如果应用使用父对象的字段访问子对象,JDO 实现会执行实体组父项查询来获取子项。

子类必须具有一个键字段,且该字段的类型可以包含父键信息(即一个键或编码为字符串的键值)。如需了解键字段类型,请参阅创建数据:键

您可以使用两个类的字段创建双向一对一关系,并在子类的字段上使用注释来声明这些字段表示双向关系。子类的字段必须具有一个包含参数 mappedBy = "..."@Persistent 批注,其中,该参数的值是父类上的字段的名称。如果一个对象的字段填充了内容,则另一个对象的对应引用字段也会自动填充内容。

ContactInfo.java

import Employee;

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

子对象是在首次被访问时从数据存储区中加载的。如果您访问的子对象不属于某父对象,则该子对象的实体始终不会加载。如果要加载子项,您可以在关闭 PersistenceManager 之前“触及”子项(例如,上例中调用 getContactInfo()),或者将子字段明确添加到默认提取组,以便通过父项检索并加载子项:

Employee.java

import ContactInfo;

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

有主的一对多关系

要在一个类的对象与另一个类的多个对象之间创建一对多关系,请使用相关类的集合:

Employee.java

import java.util.List;

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

一对多的双向关系类似于一对一关系,父类上的一个字段使用 @Persistent(mappedBy = "...") 批注,其中,其值是子类上的字段的名称:

Employee.java

import java.util.List;

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

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

定义数据类:集合中列出的集合类型支持一对多关系。 不过,数组支持一对多关系。

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

@Order 批注(使用 list-ordering 扩展)将所需的集合元素顺序指定为一个 JDOQL 排序子句。这种排序方式依据的是元素的属性值。 和查询一样,对于排序子句中使用的属性,集合的所有元素都必须具有值。

访问集合便是执行查询。如果字段的排序子句使用多个排序顺序,则查询需要数据存储区索引;如需了解详情,请参阅数据存储区索引页面。

为了提高效率,在可能的情况下,应始终使用一个显式排序子句来表示有序集合类型的一对多关系。

无主关系

除了有主关系之外,JDO API 还提供了用于管理无主关系的工具。此工具的工作方式有所不同,具体取决于您使用的是哪个版本的 App Engine DataNucleus 插件:

  • DataNucleus 插件的版本 1 不使用自然语法实现无主关系,但您仍然可以使用 Key 值代替模型对象实例(或实例集合)来管理这些关系。您可以将存储 Key 对象视为对两个对象之间的任意“外键”建模。数据存储区无法保证这些 Key 引用的完整性,但使用 Key 可让两个对象之间任何关系的建模(和随后的提取)过程变得十分简单。

    但是,如果您采用这种方式,则必须确保键的种类正确。JDO 和编译器不会为您检查 Key 的类型。
  • 2.x 版 DataNucleus 插件使用自然语法实现无主关系。

提示:在某些情况下,您可能发现有必要将有主关系当作无主关系进行建模。这是因为有主关系涉及的所有对象都被自动放入同一个实体组中,而实体组只能支持每秒 1 到 10 次写入。例如,如果父对象每秒接收 0.75 次写入,并且子对象每秒也接收 0.75 次写入,那么可以将此关系作为无主关系进行建模,使父对象和子对象驻留在各自的独立实体组中。

无主的一对一关系

假设您要对人和食物进行建模,其中一个人只能有一种最喜欢的食物,而某种最受欢迎的食物并不属于这个人,因为它可以是任何人最喜欢的食物。本部分介绍如何执行此操作。

在 JDO 2.3 中

在本例中,我们为 Person 指定一个 Key 类型的成员,其中 KeyFood 对象的唯一标识符。如果 Person.favoriteFood 引用的 Person 实例和 Food 实例不在同一实体组中,那么除非 JDO 配置设置为启用跨组 (XG) 事务,否则无法在单个事务中更新此人 (Person) 及其最喜欢的食物 (Food)。

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 中

在本例中,我们创建一个 Food 类型的私有成员,而不是为 Person 指定一个键来代表其喜欢的食物:

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;

    // ...
}

无主的一对多关系

现在假设我们要让一个人有多种最喜欢的食物。同样,某种最受欢迎的食物并只不属于这个人,因为它可以是任何人最喜欢的食物:

在 JDO 2.3 中

在此示例中,我们为 Person 提供了一个类型为 Set<Food> 的成员(其中 set 包含 Food 对象的唯一标识符),而不是为 Person 提供类型为 Set<Key> 的成员来表示该人最喜欢的食物。请注意,当 Person.favoriteFoods 中包含的 Person 实例和 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.favoriteFoods 中包含的 Person 实例和 Food 实例不在同一实体组中,并且您希望在单个事务中更新这些实例,则必须将 JDO 配置设置为启用跨组 (XG) 事务

关系、实体组和事务

如果应用将包含有主关系的对象保存到数据存储区中,则系统会自动保存可通过关系访问且需要保存的其他所有对象(可以是新对象或自上次加载后已发生更改的对象)。这会对事务和实体组有重要影响。

请考虑下面的示例,该示例在上述的 EmployeeContactInfo 类之间使用了单向关系:

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

    pm.makePersistent(e);

当使用 pm.makePersistent() 方法保存新的 Employee 对象时,会自动保存新的相关 ContactInfo 对象。由于这两个对象都是新对象,因此 App Engine 将在同一个实体组中新建两个实体,将 Employee 实体用作 ContactInfo 实体的父代。类似地,如果 Employee 对象已经保存,而相关的 ContactInfo 对象是新对象,则 App Engine 将创建 ContactInfo 实体,使用现有的 Employee 实体作为父代。

但请注意,在此示例中,对 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 可以建立带引用的关系,但相关实体将不会位于同一个组中。在这种情况下,如果将 JDO 配置设置为启用跨组 (XG) 事务,则可以在同一事务中更新或删除这两个实体。如果不使用 XG 事务,则尝试在同一事务中更新或删除属于不同组的实体将会引发 JDOFatalUserException

保存子对象已发生修改的父对象时,子对象的更改也会一并进行保存。建议您允许父对象以此方式维护所有相关子对象的持久化,并在保存更改时使用事务。

从属子项和级联删除

有主关系可以是“从属”关系,也就是说,没有父项,就不能存在子项。对于从属关系,如果删除父对象,则所有子对象也会一并删除。如果通过为父对象的从属字段分配新值来打破有主的从属关系,则原有的子对象也会删除。通过将 dependent="true" 添加到引用子对象的父对象字段的 Persistent 批注中,可以将有主的一对一关系声明为从属关系:

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

通过将 @Element(dependent = "true") 批注添加到引用子集合的父对象字段,可以将有主的一对多关系声明为从属关系:

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

与创建和更新对象一样,如果需要在一次原子性操作中执行级联删除中的所有删除,则必须在事务中执行删除操作。

注意:删除从属子对象的操作是由 JDO 实现执行的,而不是数据存储区。如果您使用低级别 API 或 Google Cloud Console 删除父实体,则相关的子对象将不会删除。

多态关系

尽管 JDO 规范包括对多态关系的支持,但 App Engine DO 实现尚不支持多态关系。这是我们希望在将来的产品版本中解除的一个限制。如果需要通过一个通用基类引用多种类型的对象,我们建议使用相同的策略来实现无主的关系:即存储一个 Key 引用。例如,如果您有一个具有 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 不再是关系字段,所以它可以引用任何类型的对象。其缺点是:与无主关系一样,需要手动管理此关系。