使用 JDO 定义数据类

您可以使用 JDO 在数据存储区中存储普通 Java 数据对象(有时称为“普通旧版 Java 对象”或“POJO”)。通过 PersistenceManager 成为持久对象的每个对象都将成为数据存储区中的一个实体。您可以使用注释告诉 JDO 如何存储和重新创建数据类的实例。

注意:较低版本的 JDO 使用 .jdo XML 文件,而不是 Java 注释。它们仍适用于 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 文档。根据 JDO 规范,默认情况下并非所有 App Engine 数据存储区核心值类型都是持久的,因此建议将字段明确注释为 @Persistent@NotPersistent 以便清晰区分。

字段的类型可以是下列任何一项。详细说明如下所示。

  • 数据存储区支持的核心类型之一
  • 核心数据存储区类型的值的集合(如 java.util.List<...>)或数组
  • @PersistenceCapable 类的一个实例或实例集合
  • Serializable 类的示例或实例集合
  • 一个嵌入式类,它存储为实体上的属性

数据类必须有且仅有一个专用字段来存储相应数据存储区实体的主键。您可以在 4 种不同类型的键字段中选择,每种字段使用不同的值类型和注释。(如需了解详情,请参阅创建数据:键。)最灵活的键字段类型是 Key 对象,在第一次将对象保存到数据存储区中时,JDO 使用和该类的其他所有实例不一样的值来自动填充该值。类型为 Key 的主键需要 @PrimaryKey 注释和 @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) 注释:

提示:将所有持久字段设置为 privateprotected(或受软件包保护),并且仅通过访问器方法提供公共访问。如果直接从另一个类访问持久字段,则可能无法体验到 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;
    }
}

核心值类型

要表示包含单个核心类型的值的属性,请声明一个 Java 类型字段,并使用 @Persistent 注释:

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

// ...
    @Persistent
    private Date hireDate;

可序列化的对象

字段值可以包含一个 Serializable 类实例,将该实例的序列化值存储在 BLOB 类型的单个属性值中。要指示 JDO 序列化该值,该字段需使用注释 @Persistent(serialized=true)。blob 值未编入索引,不能在查询过滤条件或排序顺序中使用。

下面是一个简单的 Serializable 类示例,它表示一个包含文件内容、文件名和 MIME 类型的文件。这不是 JDO 数据类,因此没有持久注释。

import java.io.Serializable;

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

    // ... accessors ...
}

要将 Serializable 类的实例作为 Blob 值存储在属性中,请声明一个类型为该类的字段,并使用 @Persistent(serialized = "true") 注释:

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

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

子对象和关系

如果某个字段值是 @PersistenceCapable 类的实例,则它会在两个对象之间创建一对一的有主关系。作为此类引用的集合的字段则创建一对多的有主关系。

重要事项:有主关系具有事务、实体组和级联删除的隐含意义。如需了解详情,请参阅事务关系

下面是一个简单的示例,它显示了 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;

    @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 实例,并使用新的 ContactInfo 实例填充其 myContactInfo 字段,然后使用 pm.makePersistent(...) 保存 Employee 实例,则数据存储区将创建两个实体。一个实体的种类是 "ContactInfo",它表示 ContactInfo 实例。另一个是 "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 中,它由具有 Collection 类型的单个字段表示,其中集合是核心值类型或 Serializable 类之一。支持以下集合类型:

  • 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> 的字段被存储为属性的零个或多个字符串值,每一个对应于 List 中的一个值。

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

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

子对象(@PersistenceCapable 类)的集合会创建具有一对多关系的多个实体。请参阅关系

对于查询过滤条件和排序顺序,有多个值的数据存储区属性具有特殊行为。如需了解详情,请参阅数据存储区查询页面。

对象字段和实体属性

App Engine 数据存储区可以区分不具有指定属性的实体和属性值为 null 的实体。JDO 不支持这种区分:对象的每个字段都有一个值,可能是 null。如果将某个具有可设为 null 的值类型(不同于 intboolean 之类的内置类型)的字段设置为 null,则在保存对象时,所得实体会将该属性设置为 null 值。

如果将一个数据存储区实体载入到对象中,并且没有适用于该对象的某个字段的属性,且该字段的类型是可设为 null 的单值类型,则该字段将设置为 null。将对象保存回数据存储区时,null 属性在数据存储区中设置为 null 值。如果该字段不是可为 null 的值类型,则加载不带相应属性的实体时将引发异常。如果实体是根据重新创建实例时所用的 JDO 类创建的,则不出现此情况;但是如果 JDO 类发生变化,或者实体是通过低级别的 API(而非 JDO)创建的,则会出现此情况。

如果某字段的类型是核心数据类型或 Serializable 类的集合,但实体上的属性没有值,则会通过将该属性设置为单一 null 值在数据存储区中表示该空集合。如果该字段的类型是数组类型,则向其分配不带任何元素的数组。如果已加载对象,但属性上没有值,则为该字段分配相应类型的空集合。在内部,数据存储区知道空集合与包含一个 null 值的集合之间的区别。

如果实体的某个属性在对象中没有对应字段,则无法从该对象访问该属性。如果将对象保存回数据存储区,则将删除额外的属性。

如果在实体的某个属性上,其值的类型与对象中相应字段的类型不同,则 JDO 会尝试将该值转换成字段类型。如果无法将该值转换为字段类型,JDO 将引发 ClassCastException。如果值是数字(长整型或双精度浮点型),则将转换该值,但不强制转换。如果数字属性值超出字段类型的范围,则转换将溢出,但不引发异常。

您可通过添加以下行将属性声明为“不编入索引”:

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

该行位于类定义中属性的上方。要详细了解它对于不编入索引的属性的意义,请参阅主文档的未编入索引的属性部分。

继承

必然要创建使用继承的数据类,而 JDO 支持此操作。在介绍 JDO 继承在 App Engine 上的工作原理之前,建议您阅读有关此主题的 DataNucleus 文档,然后再阅读本文。阅读完了吗?好的。JDO 继承在 App Engine 上按照 DataNucleus 文档中所述方式工作,并且会受到一些额外的限制。我们将讨论这些限制,然后给出一些具体的示例。

“new-table”继承策略允许拆分您跨多个“表”的单个数据对象的数据,但由于 App Engine Datastore 不支持连接,因此使用此继承策略在数据对象上操作需要对每个层级继承实施远程过程调用。这可能非常低效,因此只有其继承层次结构根上的数据类才支持“new-table”继承策略。

其次,“superclass-table”继承策略允许您将数据对象的数据存储在其超类的“表”中。尽管此策略不是固有的低效,但目前并不受支持。未来版本中,我们可能会再次见到此策略。

现在,好消息是:“subclass-table”和“complete-table”策略按照 DataNucleus 文档中所述方式的工作,而对于其继承层次结构根上的任何数据对象,您还可使用“new-table”。让我们看一个示例:

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

在此示例中,我们在 Worker 类声明中添加了一个 @Inheritance 注释,其 strategy> 特性设置为 InheritanceStrategy.SUBCLASS_TABLE。这样会指示 JDO 将 Worker 的所有持久字段存储在其子类的数据存储区实体中。由于使用 Employee 实例调用 makePersistent() 而创建的数据存储区实体将具有两个属性,它们分别名为“department”和“salary”。因使用 Intern 实例调用 makePersistent() 而创建的数据存储区实体将有两个名为“department” 和“internshipEndDate”的属性。数据存储区不包含任何种类为“Worker”的实体。

现在,让我们把它变得更有趣一点。假设,除 EmployeeIntern 以外,我们还想专门有一个特殊的 Employee,用来表示那些已经离开公司的员工:

FormerEmployee.java

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

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

在此示例中,我们在 FormerEmployee 类声明中添加了一个 @Inheritance 注释,其 custom-strategy> 特性设置为“complete-table”。这会指示 JDO 将 FormerEmployee 和其父类的所有持久字段存储在对应于 FormerEmployee 实例的数据存储区实体中。因使用 FormerEmployee 实例调用 makePersistent() 而创建的数据存储区实体将有三个分别名为“department”、“salary”和“lastDay”的属性。种类为“Employee”的实体均不与 FormerEmployee 对应。但是,如果使用运行时环境类型为 Employee 的对象调用 makePersistent(),则会创建种类为“Employee”的实体。

只要关系字段的声明类型与您将分配给这些字段的对象的运行时相匹配,就可以混合使用关系和继承。如需了解详情,请参阅有关多态关系的部分。