JDO 中的 Datastore 查询

本文档重点介绍如何使用 Java 数据对象 (JDO) 持久性框架进行 App Engine Datastore 查询。如需了解有关查询的更多常规信息,请参阅 Datastore 查询主页。

查询会从 Datastore 中检索满足一组指定条件的实体。查询作用于给定种类的实体,可基于实体的属性值、键和祖先实体指定过滤条件,并可作为结果返回零个或零个以上的实体。查询还可指定排序顺序,以便按实体的属性值对结果进行排序。结果包括符合以下条件的所有实体:针对过滤条件和排序顺序中指定的每个属性均至少有一个值(可能为 null),并且其属性值符合所有指定的过滤条件。查询可以返回整个实体、投影的实体,也可以仅返回实体

典型的查询包括以下内容:

执行时,查询会检索给定种类的实体中满足所有给定过滤条件的所有实体,并按指定顺序排序。查询以只读方式执行。

注意:为了节约内存并提高性能,查询应尽可能指定对返回结果数量的限制。

注意:基于索引的查询机制支持多种查询,适用于大多数应用,但它不支持其他数据库技术中常见的某些种类的查询:特别是 Datastore 查询引擎不支持联接和聚合查询。如需了解 Datastore 查询的相关限制,请参阅 Datastore 查询页面。

使用 JDOQL 的查询

JDO 包含一种用于检索满足一组条件的对象的查询语言。这种语言被称为 JDOQL,指的是 JDO 数据类和字段,包括针对查询参数和结果的类型检查。JDOQL 类似于 SQL,但更适用于 App Engine Datastore 等面向对象的数据库。(App Engine 的 JDO API 实现不直接支持 SQL 查询。)

JDO Query 界面支持多种调用模式:您可以使用 JDOQL 字符串语法在字符串中指定一个完整查询,也可以通过对 Query 对象调用方法来指定查询的部分或所有元素。以下示例使用含一个过滤条件和一个排序顺序的方法式调用模式,对过滤条件中所用的值使用参数替换。传递给 Query 对象的 execute() 方法的参数值将按照指定的顺序替换到查询中:

import java.util.List;
import javax.jdo.Query;

// ...

Query q = pm.newQuery(Person.class);
q.setFilter("lastName == lastNameParam");
q.setOrdering("height desc");
q.declareParameters("String lastNameParam");

try {
  List<Person> results = (List<Person>) q.execute("Smith");
  if (!results.isEmpty()) {
    for (Person p : results) {
      // Process result p
    }
  } else {
    // Handle "no results" case
  }
} finally {
  q.closeAll();
}

以下是使用字符串语法的同一查询:

Query q = pm.newQuery("select from Person " +
                      "where lastName == lastNameParam " +
                      "parameters String lastNameParam " +
                      "order by height desc");

List<Person> results = (List<Person>) q.execute("Smith");

您可以混合使用这些模式来定义查询。例如:

Query q = pm.newQuery(Person.class,
                      "lastName == lastNameParam order by height desc");
q.declareParameters("String lastNameParam");

List<Person> results = (List<Person>) q.execute("Smith");

您可以通过多次调用 execute() 方法来重复使用具有不同参数替换值的单个 Query 实例。每次调用都将执行该查询并以集合形式返回结果。

JDOQL 字符串语法支持字符串值和数值的字面值规范;所有其他值类型都必须使用参数替换。查询字符串中的字面值可用单引号 (') 或双引号 (") 引起来。以下是使用字符串字面值的示例:

Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' order by height desc");

过滤条件

“属性过滤条件”需要指定以下三项:

  • 属性名称
  • 比较运算符
  • 属性值
例如:

Filter propertyFilter =
    new FilterPredicate("height", FilterOperator.GREATER_THAN_OR_EQUAL, minHeight);
Query q = new Query("Person").setFilter(propertyFilter);
Query q = pm.newQuery(Person.class);
q.setFilter("height <= maxHeight");

属性值必须由应用提供;不能引用其他属性或者根据其他属性计算得出。如果实体具有给定名称的属性,并且其值与过滤条件中指定值的比较结果与比较运算符所述的方式一致,则该实体满足该过滤条件。

比较运算符可以是以下任何一种:

运算符 含义
== 等于
< 小于
<= 小于或等于
> 大于
>= 大于或等于
!= 不等于

查询主页所述,单个查询不能对一个以上的属性使用不等式过滤条件(<<=>>=!=)。在同一属性上使用多个不等式过滤器(比如对取值范围的查询)是允许的。contains() 过滤条件(与 SQL 中的 IN 过滤条件相对应)支持使用以下语法:

// Query for all persons with lastName equal to Smith or Jones
Query q = pm.newQuery(Person.class, ":p.contains(lastName)");
q.execute(Arrays.asList("Smith", "Jones"));

不等于 (!=) 运算符实际上执行两次查询:一次查询中,所有其他过滤条件都保持不变,不等于过滤条件被替换为小于 (<) 过滤条件;另一次查询中,不等于过滤条件被替换为大于 > 过滤条件。所得结果随后按顺序合并。一个查询最多只能有一个不等于过滤条件,已有一个不等于过滤条件的查询不能再有任何其他不等性过滤条件。

contains() 运算符也执行多次查询:指定列表中的每一项对应一次查询,所有其他过滤条件保持不变,但 contains() 过滤条件被替换为相等性 (==) 过滤条件。结果按列表中内容的顺序合并。如果查询有多个 contains() 过滤条件,那么可以作为多个查询执行,每个查询对应于 contains() 列表中每种可能的值组合

如果单个查询包含不等号 (!=) 或 contains() 运算符,则最多包含 30 个子查询。

如需详细了解 !=contains() 查询在 JDO/JPA 框架中如何转化为多个查询,请参阅使用 != 和 IN 过滤条件进行查询一文。

在 JDOQL 字符串语法中,您可以使用 &&(逻辑“和”)和 ||(逻辑“或”)运算符分隔多个过滤条件:

q.setFilter("lastName == 'Smith' && height < maxHeight");

不支持否定(逻辑“否”)。此外,请记住,只有当 || 运算符所分隔的过滤条件都具有相同的属性名称时(即,当它们可以组合成单个 contains() 过滤条件时),才能使用该运算符:

// Legal: all filters separated by || are on the same property
Query q = pm.newQuery(Person.class,
                      "(lastName == 'Smith' || lastName == 'Jones')" +
                      " && firstName == 'Harold'");

// Not legal: filters separated by || are on different properties
Query q = pm.newQuery(Person.class,
                      "lastName == 'Smith' || firstName == 'Harold'");

排序顺序

查询“排序顺序”可指定以下两项:

  • 属性名称
  • 排序方向(升序或降序)

例如:

// Order alphabetically by last name:
Query q1 = new Query("Person").addSort("lastName", SortDirection.ASCENDING);

// Order by height, tallest to shortest:
Query q2 = new Query("Person").addSort("height", SortDirection.DESCENDING);

例如:

// Order alphabetically by last name:
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc");

// Order by height, tallest to shortest:
Query q = pm.newQuery(Person.class);
q.setOrdering("height desc");

如果查询包含多个排序顺序,则按指定的顺序加以应用。以下示例先按姓升序排序,然后按身高降序排序:

Query q =
    new Query("Person")
        .addSort("lastName", SortDirection.ASCENDING)
        .addSort("height", SortDirection.DESCENDING);
Query q = pm.newQuery(Person.class);
q.setOrdering("lastName asc, height desc");

如果未指定排序顺序,将按照从 Datastore 检索的顺序返回结果。

注意:由于 Datastore 执行查询的方式,如果查询对某个属性指定了不等性过滤条件,而对其他属性指定了排序顺序,则不等性过滤条件中使用的属性必须先于其他属性进行排序。

范围

查询可指定将返回给应用的结果的“范围”。范围指示要在完整结果集中首先返回哪些结果,最后返回哪些结果。结果通过数字索引进行标识,0 表示集合中的第一个结果。例如,范围为 5,10 返回第 6 个到第 10 个结果:

q.setRange(5, 10);

注意:使用范围可能会影响性能,因为 Datastore 必须先检索起始偏移量之前的所有结果,然后再将其丢弃。例如,范围为 5,10 的查询将首先检索 Datastore 中的 10 个结果,然后丢弃前 5 个结果,并将剩余 5 个结果返回给应用。

基于键的查询

实体键可以作为查询过滤条件或排序顺序使用的元素。Datastore 会考虑此类查询的完整键值,包括实体的祖先实体路径、种类以及应用分配的键名称字符串或系统分配的数字 ID。由于键在系统中的所有实体之间都是唯一的,因此利用键查询能够轻松批量检索给定种类的实体,例如用于 Datastore 内容的批量转储。与 JDOQL 范围不同,该查询对任何数量的实体都很有效。

在进行不等性比较时,键会依次按照以下标准排序:

  1. 祖先实体路径
  2. 实体种类
  3. 标识符(键名称或数字 ID)

祖先实体路径的元素采用类似的方式进行比较:按种类(字符串),然后按键名称或数字 ID 比较。种类和键名称是字符串,按字节值排序;数字 ID 是整数,按数字顺序排列。如果具有相同父级和种类的实体混合使用键名称字符串和数字 ID,那么使用数字 ID 的实体排在使用键名称的实体前面。

在 JDO 中,您可以使用对象的主键字段在查询中引用实体键。如需使用某个键作为查询过滤条件,请为 declareParameters() 方法指定参数类型 Key。假设 PersonFood 之间存在非所属一对一关系,以下内容将查找具有给定喜欢食物的所有 Person 实体:

Food chocolate = /*...*/;

Query q = pm.newQuery(Person.class);
q.setFilter("favoriteFood == favoriteFoodParam");
q.declareParameters(Key.class.getName() + " favoriteFoodParam");

List<Person> chocolateLovers = (List<Person>) q.execute(chocolate.getKey());

仅限于键的查询仅返回结果实体的键而非实体本身,与检索整个实体相比,延迟时间更短,费用更低:

Query q = new Query("Person").setKeysOnly();
Query q = pm.newQuery("select id from " + Person.class.getName());
List<String> ids = (List<String>) q.execute();

与执行获取的实体数量比所需数量多的常规查询相比,首先执行仅限于键的查询,然后从结果中获取实体的子集往往更为经济。

范围

JDO 范围代表 Datastore 中某个特定类的任一对象。您可以通过将所需的类传递给 Persistence Manager 的 getExtent() 方法来创建 JDO 范围。Extent 接口扩展了用于访问结果的 Iterable 接口,可根据需要分批次检索结果。完成对结果的访问后,您可以调用相应范围的 closeAll() 方法。

以下示例将迭代 Datastore 中的每个 Person 对象:

import java.util.Iterator;
import javax.jdo.Extent;

// ...

Extent<Person> extent = pm.getExtent(Person.class, false);
for (Person p : extent) {
  // ...
}
extent.closeAll();

借助查询删除实体

如果要发出目标为删除与查询过滤条件匹配的所有实体的查询,您可以使用 JDO的“利用查询进行删除”功能来减轻一些编码任务。以下代码删除身高超过给定身高的所有人:

Query q = pm.newQuery(Person.class);
q.setFilter("height > maxHeightParam");
q.declareParameters("int maxHeightParam");
q.deletePersistentAll(maxHeight);

您可以看出,此处唯一的区别是我们调用了 q.deletePersistentAll() 而不是 q.execute()。无论您要选择结果集还是要删除结果集,前文所述过滤条件、排序顺序和索引的所有规则和限制都适用于查询。 但请注意,如同您使用 pm.deletePersistent() 删除这些 Person 实体时一样,这些实体的从属子项也将被此类查询删除。如需详细了解从属子项,请参阅 JDO 中的实体关系页面。

查询游标

在 JDO 中,您可以通过一个扩展程序和 JDOCursorHelper 类对 JDO 查询使用游标。提取列表形式的结果或使用迭代器时会用到游标。如需获取游标,请将结果列表或迭代器传递给静态方法 JDOCursorHelper.getCursor()

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.jdo.Query;
import com.google.appengine.api.datastore.Cursor;
import org.datanucleus.store.appengine.query.JDOCursorHelper;

Query q = pm.newQuery(Person.class);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the first 20 results

Cursor cursor = JDOCursorHelper.getCursor(results);
String cursorString = cursor.toWebSafeString();
// Store the cursorString

// ...

// Query q = the same query that produced the cursor
// String cursorString = the string from storage
Cursor cursor = Cursor.fromWebSafeString(cursorString);
Map<String, Object> extensionMap = new HashMap<String, Object>();
extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor);
q.setExtensions(extensionMap);
q.setRange(0, 20);

List<Person> results = (List<Person>) q.execute();
// Use the next 20 results

如需详细了解查询游标,请参阅 Datastore 查询页面。

Datastore 读取政策和调用期限

您可以使用配置为通过 PersistenceManager 实例进行的所有调用设置读取政策(强一致性与最终一致性)和 Datastore 调用截止时间。您还可以为单个 Query 对象替换这些选项。(但请注意,通过键提取实体时,无法替换这些选项的配置。)

当为 Datastore 查询选择最终一致性时,也可以通过最终一致性访问该查询用来收集结果的索引。 查询偶尔会返回与查询条件不匹配的实体,采用强一致性的读取政策时也是如此。 (如果查询使用祖先实体过滤条件,则可以使用事务来确保结果集的一致性。)

如需替换单个查询替的读取政策,请调用其 addExtension() 方法:

Query q = pm.newQuery(Person.class);
q.addExtension("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

可能值为 "EVENTUAL""STRONG";默认值为 "STRONG",除非在配置文件 jdoconfig.xml 中另行设置。

如需替换单个查询的 Datastore 调用期限,请调用其 setDatastoreReadTimeoutMillis() 方法:

q.setDatastoreReadTimeoutMillis(3000);

该值是以毫秒表示的时间间隔。