关系
使用对象类型的字段,可以在持久对象之间建模关系。持久对象之间的关系可描述为有主的(其中一个对象无法脱离另一个而存在)或无主的(两个对象都可存在,而不管其彼此的关系)。JDO 接口的 App Engine 实现可以建模有主的一对一关系和有主的一对多关系,这些关系既单向又双向。无主的关系尚未为自然语法所支持,但您可以通过直接在字段中存储数据存储区键来自己管理这些关系。App Engine 在实体组中自动创建相关实体以便支持相关实体的共同更新,但应用程序有必要了解何时使用数据存储区事务。
有主的一对一关系
通过使用字段(其类型为相关类的类),可以在两个持久对象之间创建单向的一对一有主关系。
下例定义了 ContactInfo 数据类和 Employee 数据类,有一个从 Employee 到 ContactInfo 的一对一关系。
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; } // ... }
持久对象表示为数据存储区中具有两种不同类型的两个不同实体。使用实体组关系来表示该关系:子代的键使用父代的键作为其父实体组。当时用程序使用父对象的字段访问子对象时,JDO 实现执行父实体组查询以获取子对象。
子类必须具有类型包含父键信息的键字段,这些父键信息为 Key 或以字符串编码的键值。有关键字段类型的信息,请参阅创建数据:键。
使用两个类上的字段来创建双向一对一关系,用子类字段上的批注来声明这些字段表示双向关系。子类字段必须具有带参数 mappedBy = "..."
的 @Persistent
批注,该参数的值是父类字段的名称。如果填充一个对象上的字段,则会自动填充另一个对象上对应的引用字段。
ContactInfo.java
import Employee; // ... @Persistent(mappedBy = "contactInfo") private Employee employee;
当初次访问子对象时,将从数据存储区中载入这些对象。如果没有访问父对象上的子对象,则绝不会载入该子对象的实体。(数据存储区接口不支持“预先”载入子对象。数据存储区不支持 join 查询,因此预先载入的实现还是需要应用程序对数据存储区进行调用。)
有主的一对多关系
要在一个类的对象与另一个类的多个对象之间创建一对多关系,请使用相关类的集合:
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 不支持 join 查询:您无法使用子实体的属性来查询父实体。(可以查询嵌入类的属性,因为嵌入类将属性存储在父实体上。请参阅定义数据类:嵌入类。)
有序集合如何保持其顺序
诸如 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 顺序子句。顺序使用元素的属性值。和查询一样,所有集合元素都必须具有顺序子句中使用的属性的值。
访问集合将执行查询。如果字段的顺序子句使用多个排序顺序,则该查询需要数据存储区索引。有关索引的详细信息,请参阅查询和索引。
为了更加高效,在可能的情况下,始终对有序集合类型的一对多关系使用显式顺序子句。
无主的关系
除了有主关系之外,JDO API 还提供用于管理无主关系的工具。JDO 的 App Engine 实现尚没有实现此工具,但无需担心,因为您仍然可以使用 Key
值代替模型对象的实例(或实例集合)来管理这些关系。您可以将存储键对象看作在两个对象之间建模任意“外键”。数据存储区不保证与这些键引用的引用完整性,但 Key 的使用使得在两个对象之间建模(接着抓取)任何关系十分简单。但是,如果您要继续,请牢记其他几点。首先,应用程序有必要确保键的类型正确 - JDO 和编译器不会为您进行任何类型检查。其次,全部对象必须属于同一个实体组,这样才能对关系双方的对象执行原子更新。
提示:某些情况下,会发现有必要将有主关系当作无主关系进行建模。这是因为有主关系涉及的全部对象将自动放入同一个实体组中,而实体组只能支持每秒 1 到 10 次写入。因此,例如,如果父对象每秒接收 0.75 次写入并且子对象每秒也接收 0.75 次写入,则可以将此关系作为无主建模,使得父对象和子对象驻留在其自己的独立实体组中。
无主的一对一关系
假如我们要对人和食物进行建模,其中一个人只能有一种最喜欢的食物而一种最喜欢的食物并不属于该人,因为它可以是任何人最喜欢的食物:
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
提供类型为 Key
的成员(其中 Key
为 Food
对象的唯一标识符),而不是为 Person
提供类型为 Food
的成员来表示该人最喜欢的食物。请注意,除非 Person
的实例和 Person.favoriteFood
所引用的 Food
实例位于同一实体组中,否则不可能在单个事务中更新该人和该人最喜欢的食物。
无主的一对多关系
现在,假如我们要让一个人有多种最喜欢的食物。同样,一种最喜欢的食物并不属于该人,因为它可以是任何人最喜欢的食物:
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<Key>
的成员(其中 set 包含 Food
对象的唯一标识符),而不是为 Person 提供类型为Set<Food>
的成员来表示该人最喜欢的食物。请注意,除非 Person
的实例和包含在 Person.favoriteFoods
中的 Food
实例位于同一个实体组中,否则不可能在单个事务中更新该人和该人最喜欢的食物。
多对多关系
可以通过保留关系双方的键集合来建模多对多关系。让我们调整本示例,使得 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
实例位于同一个实体组中,否则不可能在单个事务中更新该人和该人最喜欢的食物。如果无法使对象位于同一个实体组中,则应用程序必须能够实现不必相应地更新将该食物视为自己最喜欢的食物的人集合即可更新某人最喜欢的食物,反之,还必须能够实现不必相应地更新该人最喜欢的食物即可更新将某食物视为自己最喜欢的食物的人集合。
关系、实体组和事务
当拥有有主关系的对象保存到数据存储区中时,会自动保存能够沿关系到达且需要保存的所有其他对象(新对象或自上次载入以来已更改的对象)。这对事务和实体组意义重大。
请思考使用上述 Employee
与 ContactInfo
类之间的单向关系的下例:
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 可以建立带有引用的关系,但相关实体不会在同一组中。在此情况下,无法在同一个事务中更新或删除这两个实体。如果尝试在同一个事务中更新或删除不同组的实体,则将引发 JDOFatalUserException。
保存子对象被修改的父对象时,会将更改保存到子对象。以此方式使父对象保留所有相关子对象的持久性并在保存更改时使用事务是很好的主意。
从属子代和级联删除
JDO 的 App Engine 实现使得所有有主关系变成“从属”。如果删除父对象,则也会删除所有子对象。通过对父对象的从属字段赋新的值来打破有主关系也会删除旧的子对象。
和创建、更新对象一样,如果需要在单个原子操作中进行级联删除中的每个删除,则必须执行事务删除。
注意:从属子对象是由 JDO 实现删除的,而不是数据存储区。如果使用低级 API 或管理控制台删除父实体,则不会删除相关子对象。