实体、属性和键

Datastore 中的数据对象被称为实体。实体具有一个或多个命名属性,且每个属性可具有一个或多个值。相同种类的实体无需具有相同的属性,且实体的给定属性的值无需都是同一数据类型。(必要时,应用可在其自身的数据模型中设定和实施此类限制)。

Datastore 支持多种属性值数据类型,其中包括:

  • 整数
  • 浮点数
  • 字符串
  • 日期
  • 二进制数据

如需查看类型的完整列表,请参阅属性和值类型

Datastore 中的每个实体都有一个唯一标识它的键。键由以下部分组成:

  • 实体的命名空间,可实现多租户
  • 实体所属的种类,用于对实体进行分类以执行 Datastore 查询
  • 具体实体的标识符,可以是下面任意一种
    • 键名字符串
    • 整数数字 ID
  • (可选)祖先路径,用于在 Datastore 层次结构中确定实体的位置

应用可以使用具体实体的键从 Datastore 中提取该实体,也可以根据实体的键或属性值发出查询以检索一个或多个实体。

Java App Engine SDK 包括一个简单的 API,此 API 由 com.google.appengine.api.datastore 包提供,可直接支持 Datastore 的功能。本文档中的所有示例均基于此低层级 API;您可以选择直接在应用中使用该 API,或者以此作为基础,构建您自己的数据管理层。

Datastore 本身并不对实体结构施加任何限制,例如限制给定属性采用特定类型的值;该任务由应用执行。

种类和标识符

每个 Datastore 实体均属于特定“种类”,如此可将实体进行分类以便于查询;例如,人力资源应用可以通过种类为 Employee 的实体来表示公司的每位员工。在 Java Datastore API 中创建实体时,您可以通过作为参数提供给 Entity() 构造函数的方式来指定实体的种类。以两个下划线 (__) 字符开头的所有种类名称均属于保留名称,不得使用。

以下示例创建种类为 Employee 的实体,填充其属性值,并将其保存到 Datastore:

Entity employee = new Entity("Employee", "asalieri");
employee.setProperty("firstName", "Antonio");
employee.setProperty("lastName", "Salieri");
employee.setProperty("hireDate", new Date());
employee.setProperty("attendedHrTraining", true);

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(employee);

除了种类,在创建实体时还会为每个实体分配标识符。标识符是实体键的一部分,因此与实体永久关联且不可更改。标识符可通过下述两种方式分配:

  • 应用可为实体指定自己的键名字符串
  • 您可让 Datastore 自动为实体分配一个整数数字 ID。

如需为实体分配键名,请在创建实体时将名称作为第二个参数提供给构造函数:

Entity employee = new Entity("Employee", "asalieri");

如需让 Datastore 自动分配数字 ID,请忽略此参数:

Entity employee = new Entity("Employee");

分配标识符

Datastore 可配置为使用两种不同的自动 ID 政策生成自动 ID:

  • default 政策会生成大致均匀分布的未使用 ID 的随机序列。每个 ID 最多可包含 16 位十进制数字。
  • legacy 政策会创建一系列不连续的较小整数 ID。

如果希望向用户显示实体 ID 和/或按照顺序显示实体 ID,最好是使用手动分配。

Datastore 生成大致均匀分布的未使用 ID 的随机序列。每个 ID 最多可包含 16 位十进制数字。

祖先路径

Cloud Datastore 中的实体形成一个与文件系统目录结构类似的层级结构空间。创建实体时,您可选择指定另一实体作为其父实体;新实体是父实体的子实体(请注意,与文件系统不同,无需实际存在父实体)。没有父实体的实体是根实体。实体与其父实体之间的关联是永久的,实体创建后就无法更改。Cloud Datastore 绝不向父实体相同的两个实体分配同一数字 ID,也不分配给两个根实体(即没有父实体的实体)。

实体的父实体、父实体的父实体和以此类推得出的实体都是该实体的祖先实体;而实体的子实体和子实体的子实体等都是它的后代实体。根实体及其所有后代实体都属于同一个实体组。实体序列从根实体开始,接着从父实体到子实体,再指向给定的实体,这就构成了实体的祖先路径。识别实体的完整键由一系列种类/标识符对构成,它们指定实体的祖先路径并以实体自身的种类/标识符对终止。

[Person:GreatGrandpa, Person:Grandpa, Person:Dad, Person:Me]

对于根实体,祖先路径为空,且键仅由实体自身的种类和标识符组成:

[Person:GreatGrandpa]

此概念如下图所示:

显示实体组中的根实体与子实体的关系

如需指定实体的父实体,请在创建子实体时将父实体的键作为参数提供给 Entity() 构造函数。您可以通过调用父实体的 getKey() 方法来获取键:

Entity employee = new Entity("Employee");
datastore.put(employee);

Entity address = new Entity("Address", employee.getKey());
datastore.put(address);

如果新实体也有键名,请将键名作为第二个参数提供给 Entity() 构造函数,并提供父实体的键作为第三个参数:

Entity address = new Entity("Address", "addr1", employee.getKey());

事务和实体组

每次尝试创建、更新或删除实体时,这些操作都将在事务上下文中进行。单个事务可以包括任意数量的此类操作。为保持数据的一致性,事务确保其包含的所有操作作为一个单元应用于 Datastore,只要有任何操作失败,则所有操作都不会应用。此外,在同一个事务中执行的所有强一致读取(祖先查询或 get 方法)都遵从一致的数据快照。

如上所述,实体组是一组通过祖先实体连接到共同根元素的实体。将数据整理成实体组可以限制可执行的事务:

  • 一个事务所访问的所有数据都必须包含在最多 25 个实体组中。
  • 如果想在事务内使用查询,必须将数据整理成实体组,如此用户才可以指定与正确数据匹配的祖先过滤器。
  • 单个实体组的写入吞吐量限制为每秒约一个事务。存在此限制是因为 Datastore 会在大范围的地理区域内,对每个实体组执行无主同步复制,以提供高可靠性和容错性。

在许多应用中,可在广泛查看互不相关数据时使用最终一致性(即跨多个实体组进行非祖先查询,有时可能会返回稍过时的数据),然后在查看或修改一组高度相关的数据时,使用强一致性(祖先查询,或使用 get 方法查询单个实体)。在此类应用中,通常适合为每组高度相关的数据使用独立的实体组。如需了解详情,请参阅设计结构以确保高度一致性

属性和值类型

与实体关联的数据值由一个或多个属性构成。每个属性都有一个名称和一个或多个值。一个属性可具有多个类型的值,且两个实体的同一属性可具有不同类型的值。属性可以编入索引,也可以不编入索引(对属性 P 排序或过滤的查询会忽略未将 P 编入索引的实体)。一个实体最多可以有 20000 个编入索引的属性。

支持以下值类型:

值类型 Java 类型 排序顺序 备注
整数 short
int
long
java.lang.Short
java.lang.Integer
java.lang.Long
数字 以长整数形式存储,然后转换为字段类型

超出范围的值溢出
浮点数 float
double
java.lang.Float
java.lang.Double
数字 64 位双精度,
IEEE 754
布尔值 boolean
java.lang.Boolean
false<true
短文本字符串 java.lang.String Unicode 最多 1500 字节

长度超过 1500 字节的值会引发 IllegalArgumentException
长文本字符串 com.google.appengine.api.datastore.Text 最多 1 兆字节

未编入索引
短字节字符串 com.google.appengine.api.datastore.ShortBlob 字节顺序 最多 1500 字节

长度超过 1500 字节的值会引发 IllegalArgumentException
长字节字符串 com.google.appengine.api.datastore.Blob 最多 1 兆字节

未编入索引
日期和时间 java.util.Date 时间顺序
地理位置点 com.google.appengine.api.datastore.GeoPt 先按纬度排序,
再按经度排序
邮寄地址 com.google.appengine.api.datastore.PostalAddress Unicode
电话号码 com.google.appengine.api.datastore.PhoneNumber Unicode
电子邮件地址 com.google.appengine.api.datastore.Email Unicode
Google 账号用户 com.google.appengine.api.users.User 按 Unicode 顺序排序的
电子邮件地址
即时消息传输句柄 com.google.appengine.api.datastore.IMHandle Unicode
链接 com.google.appengine.api.datastore.Link Unicode
类别 com.google.appengine.api.datastore.Category Unicode
评分 com.google.appengine.api.datastore.Rating 数字
Datastore 键 com.google.appengine.api.datastore.Key
或引用的对象(作为子对象)
按路径元素
(种类、标识符、
种类、标识符...)排序
最多 1500 字节

长度超过 1500 字节的值会引发 IllegalArgumentException
Blobstore 键 com.google.appengine.api.blobstore.BlobKey 字节顺序
嵌入式实体 com.google.appengine.api.datastore.EmbeddedEntity 未编入索引
null

重要提示:强烈建议您不要将 users.User 作为属性值进行存储,因为它包含电子邮件地址和唯一 ID。如果用户更改了其电子邮件地址,当您将其存储的旧 user.User 与新 user.User 值进行比较时,它们将无法匹配。请改为将 User 用户 ID 值用作用户固定的唯一标识符。

对于文本字符串和未编码的二进制数据(字节字符串),Datastore 支持两种值类型:

  • 短字符串(最多 1500 字节)会编入索引,可以在查询过滤条件和排序顺序中使用。
  • 长字符串(最多 1 兆字节)不编入索引,不能在查询过滤条件和排序顺序中使用。
注意:在 Datastore API 中,长字节字符串类型名为 Blob。此类型与 Blobstore API 中使用的 Blob 无关。

如果查询涉及的属性具有混合类型的值,Datastore 会根据内部表示法确定排序方式:

  1. Null 值
  2. 定点数
    • 整数
    • 日期和时间
    • 评分
  3. 布尔值
  4. 字节序列
    • 字节字符串
    • Unicode 字符串
    • Blobstore 键
  5. 浮点数
  6. 地理位置点
  7. Google 账号用户
  8. Datastore 键

由于长文本字符串、长字节字符串和嵌入式实体未编入索引,因此它们没有定义排序顺序。

处理实体

应用可以使用 Datastore API 创建、检索、更新和删除实体。如果应用知道完整的实体键(或可从其父键、种类和标识符中提取),则可使用此键直接操作实体。应用还可以通过 Datastore 查询获取实体的键;如需了解详情,请参阅 Datastore 查询页面。

Java Datastore API 使用 DatastoreService 接口的方法来操作实体。您可以通过调用静态方法 DatastoreServiceFactory.getDatastoreService() 来获取 DatastoreService 对象:

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

创建实体

如需创建新实体,您可以构造 Entity 类的实例,并将实体的种类作为参数提供给 Entity() 构造函数。

根据需要填充实体的属性后,您可以将其作为参数传递给 DatastoreService.put() 方法,从而保存到 Datastore。您可以将实体的键名作为第二个参数传递给构造函数,从而指定该键名:

Entity employee = new Entity("Employee", "asalieri");
// Set the entity properties.
// ...
datastore.put(employee);

如果您未提供键名,则 Datastore 将自动为该实体的键生成数字 ID:

Entity employee = new Entity("Employee");
// Set the entity properties.
// ...
datastore.put(employee);

检索实体

如需检索由给定键标识的实体,请将 Key 对象传递给 DatastoreService.get() 方法:

// Key employeeKey = ...;
Entity employee = datastore.get(employeeKey);

更新实体

如需更新现有实体,请修改实体对象的特性,然后将其传递给 DatastoreService.put() 方法。该对象数据将覆盖现有实体。每次调用 put() 时,整个对象都会发送到 Datastore。

删除实体

如果您知道实体的键,可以使用 DatastoreService.delete() 方法删除实体:

// Key employeeKey = ...;
datastore.delete(employeeKey);

重复属性

您可以在单个属性中存储多个值。

Entity employee = new Entity("Employee");
ArrayList<String> favoriteFruit = new ArrayList<String>();
favoriteFruit.add("Pear");
favoriteFruit.add("Apple");
employee.setProperty("favoriteFruit", favoriteFruit);
datastore.put(employee);

// Sometime later
employee = datastore.get(employee.getKey());
@SuppressWarnings("unchecked") // Cast can't verify generic type.
    ArrayList<String> retrievedFruits = (ArrayList<String>) employee
    .getProperty("favoriteFruit");

嵌入式实体

有时,您可能会发现,将一个实体作为另一个实体的属性嵌入十分方便。这在某些情况下很有用,例如创建实体中属性值的层级结构。您可以借助 Java 类 EmbeddedEntity 来实现此目的:

// Entity employee = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setProperty("homeAddress", "123 Fake St, Made, UP 45678");
embeddedContactInfo.setProperty("phoneNumber", "555-555-5555");
embeddedContactInfo.setProperty("emailAddress", "test@example.com");

employee.setProperty("contactInfo", embeddedContactInfo);

将嵌入式实体添加到索引后,您就可以查询子属性。如果您将嵌入式实体从索引中排除,则所有子属性也会从索引中排除。您可以选择将一个键与嵌入式实体相关联,但与完整实体不同,键不是必需的,而且即使键存在,也不能用于检索该实体。

您可以使用 setPropertiesFrom() 方法从现有实体中复制嵌入式实体的属性,而无需手动填充:

// Entity employee = ...;
// Entity contactInfo = ...;
EmbeddedEntity embeddedContactInfo = new EmbeddedEntity();

embeddedContactInfo.setKey(contactInfo.getKey()); // Optional, used so we can recover original.
embeddedContactInfo.setPropertiesFrom(contactInfo);

employee.setProperty("contactInfo", embeddedContactInfo);

之后,您可以使用相同方法从嵌入式实体恢复原始实体:

Entity employee = datastore.get(employeeKey);
EmbeddedEntity embeddedContactInfo = (EmbeddedEntity) employee.getProperty("contactInfo");

Key infoKey = embeddedContactInfo.getKey();
Entity contactInfo = new Entity(infoKey);
contactInfo.setPropertiesFrom(embeddedContactInfo);

批量操作

DatastoreServiceput()get()delete() 方法(及其 AsyncDatastoreService 对应方法)具有批量版本,可以接受可迭代的对象(对于 put(),属于 Entity 类;对于 get()delete(),属于 Key 类),并用于在单次 Datastore 调用中操作多个实体:

Entity employee1 = new Entity("Employee");
Entity employee2 = new Entity("Employee");
Entity employee3 = new Entity("Employee");
// ...

List<Entity> employees = Arrays.asList(employee1, employee2, employee3);
datastore.put(employees);

这些批量操作按实体组对所有实体或键进行分组,然后对每个实体组并行执行请求的操作。较之分别调用每个单独的实体,这种批量调用速度更快,因为它们仅产生一次服务调用的开销。如果涉及多个实体组,则在服务器端对所有组并行执行该操作。

生成键

应用可以使用 KeyFactory 类,基于已知组件(如实体的种类和标识符)为实体创建 Key 对象。对于没有父实体的实体,可以将种类和标识符(键名字符串或数字 ID)传递给静态方法 KeyFactory.createKey() 以创建键。以下示例为种类为 Person 且键名为 "GreatGrandpa" 或数字 ID 为 74219 的实体创建键:

Key k1 = KeyFactory.createKey("Person", "GreatGrandpa");
Key k2 = KeyFactory.createKey("Person", 74219);

如果键包含路径组件,您可以使用辅助类 KeyFactory.Builder 来构建路径。该类的 addChild 方法会在路径中添加单个实体,并返回构建器本身,因此您可以将一系列调用连在一起(从根实体开始),为路径一次构建一个实体。构建完整路径后,请调用 getKey 以检索生成的键:

Key k =
    new KeyFactory.Builder("Person", "GreatGrandpa")
        .addChild("Person", "Grandpa")
        .addChild("Person", "Dad")
        .addChild("Person", "Me")
        .getKey();

KeyFactory 类还包括静态方法 keyToStringstringToKey,用于在键与其字符串表示法之间切换:

String personKeyStr = KeyFactory.keyToString(k);

// Some time later (for example, after using personKeyStr in a link).
Key personKey = KeyFactory.stringToKey(personKeyStr);
Entity person = datastore.get(personKey);

键的字符串表示法可在 Web 上安全使用:该表示法不包含 HTML 或网址中不允许使用的特殊字符。

使用空列表

过去,Datastore 未提供表示空列表的属性表示法。Java SDK 通过将空集合存储为 null 值解决了此问题,但也因此无法区分 null 值和空列表。为保持向后兼容性,仍保留了此默认行为,概括如下:

  • null 属性作为 null 写入 Datastore
  • 空集合作为 null 写入 Datastore
  • 从 Datastore 中读取的 null 为 null
  • 读取的空集合为 null。

但是,如果您更改默认行为,则适用于 Java 的 SDK 将支持存储空列表。我们建议您充分考虑更改应用默认行为的影响,然后再开启对空列表的支持

如需更改默认行为以便使用空列表,请在应用初始化期间设置 DATASTORE_EMPTY_LIST_SUPPORT 属性,如下所示:

System.setProperty(DatastoreServiceConfig.DATASTORE_EMPTY_LIST_SUPPORT, Boolean.TRUE.toString());

按上述方法将此属性设为 true 后,会发生以下情况:

  • null 属性作为 null 写入 Datastore
  • 空集合作为空列表写入 Datastore
  • 从 Datastore 中读取的 null 为 null
  • 从 Datastore 中读取空列表时将返回空集合。