应用可以使用查询在 Datastore 中搜索与特定搜索条件(称为过滤条件)匹配的实体。
概览
应用可以使用查询在 Datastore 中搜索与特定搜索条件(称为过滤条件)匹配的实体。例如,跟踪多个留言板的应用可以使用查询从一个留言板中检索消息,并按日期排序:
...
...
有些查询比其他查询更复杂一些;Datastore 需要使用针对这些复杂查询预先构建的索引。
这些预构建的索引在配置文件 index.yaml 中指定。在���发服务������,如果运行的查询所需的索引尚未指定,开发服务器会自动将该索引添加到其 index.yaml 中。但是在您的网站中,如果查询所需的索引尚未指定,则该查询将失败。
因此,典型的开发周期是在开发服务器上试验新查询,然后将网站更新为使用自动更改后的 index.yaml。您可以通过运行 gcloud app deploy index.yaml 单独更新index.yaml,而不必上传应用。如果您的数据存储区包含多个实体,为它们创建新索引需要花费很长时间;在这种情况下,您可以先更新索引定义,然后再上传使用新索引的代码。
您可以使用管理控制台查找索引完成构建的时间。
App Engine Datastore 本身支持使用过滤条件进行精确匹配(== 运算符)和比较(<、<=、> 和 >= 运算符)。
它支持使用布尔 AND 运算组合多个过滤条件,但有一些限制(见下文)。
除原生运算符之外,该 API 还支持 != 运算符(使用布尔 !OR 运算组合多组过滤条件)和 IN 运算(用于测试是否等于可能值列表中的某个值,类似于 Python 的“in”运算符)。这些运算与 Datastore 的原生运算之间没有一一对应的关系,因此结果显得有点古怪且相对较慢。这些运算通过在内存中合并结果流来实现。 请注意,p != v 实现为 "p < v OR p > v"。
(这对于重复属性 (property) 而言很重要。)
限制:Datastore 对查询施加了一些限制。违反这些限制将导致其引发异常。 例如,当前不允许的情况包括:组合过多的过滤条件,将不等式用于多个属性,或者将不等式与另一个不同属性的排序顺序组合使用。此外,引用多个属性的过滤条件有时也需要配置二级索引。
不支持:Datastore 不直接支持子字符串匹配、不区分大小写的匹配或所谓的全文搜索。 但您可以通过某些方法使用���算属性实现��区分大小写的匹配�����全文搜索。
按属性值过滤
调用 NDB 属性中的 Account 类:
通常,您并不希望检索指定种类的所有实体,而只想要检索某属性具有特定值或值范围的那些实体。
属性对象重载了一些运算符,以返回可用于控制查询的过滤条件表达式:例如,要查找 userid 属性具有确切值 42 的所有 Account 实体,可以使用表达式
(如果您确定只有一个 Account 具有该 userid,则建议使用 userid 作为键。Account.get_by_id(...) 快于 Account.query(...).get()。)
NDB 支持以下运算:
property == value
property < value
property <= value
property > value
property >= value
property != value
property.IN([value1, value2])
要过滤不等式,可以使用如下语法:
这将查找 userid 属性大于或等于 40 的所有 Account 实体。
其中两个运算 != 和 IN 实现为其他运算的组合,看起来有点奇怪,如 != 和 IN 中所述。
您可以指定多个过滤条件:
此示例组合了指定的过滤条件参数,返回 userid 值大于或等于 40 且小于 50 的所有 Account 实体。
注意:如前所述,Datastore 拒绝对多个属性使用不等式过滤的查询。
您可能会发现,分步构建查询过滤条件,比在单个表达式中指定整个查询过滤条件更方便,例如:
query3 等同于前一个示例中的 query 变量。请注意,查询对象是不可变的,因此 query2 的构造不会影响 query1,而 query3 的构造不会影响 query1 或 query2。
!= 和 IN 运算
调用 NDB 属性中的 Article 类:
!=(不等于)和 IN(成员资格)运算是通过使用 OR 运算组合其他过滤条件来实现的。前者
property != value
实现为
(property < value) OR (property > value)
例如:
等效于
注意:您也许感到意外,但此查询搜索的并非是不包含“perl”作为标记的 Article 实体!相反,它会找出至少有一个标记不等于“perl”的所有实体。
例如,以下实体虽然将“perl”作为其标记之一,但也将包含在结果中:
但是,以下实体不包括在内:
无法查询不包含等于“perl”的标记的实体。
同样,用于测试是否属于可能值列表中的成员之一的 IN 运算
property IN [value1, value2, ...]
实现为
(property == value1) OR (property == value2) OR ...
例如:
等效于
注意:使用 OR 的查询会删除其结果中重复的数据:结果流不会多次包含同一个实体,即使实体与两个或更多子查询匹配也是如此。
查询重复属性
前面部分中定义的 Article 类也可以作为查询重复属性的示例。值得注意的是,类似于
的过滤条件使用单个值,虽然 Article.tags 是重复属性。您无法将重复属性与列表对象进行比较(Datastore 无法理解),并且类似于
的过滤条件,执行的操作与搜索标记值是列表 ['python', 'ruby', 'php'] 的 Article 实体完全不同:该过滤条件会搜索 tags 值(视为列表)中至少包含以上值之一的实体。
对重复属性查询 None 值会产生未知行为,我们不建议这么做。
组合 AND 和 OR 运算
您可以任意嵌套 AND 和 OR 运算。例如:
但是,由于 OR 的实现方法,如果此形式的查询过于复杂,就可能失败并引发异常。更安全的做法是对这些过滤条件进行标准化,使��在表达式树的顶部(最多)只有一个 OR 运算,并且在此之下只有一级 AND 运算。
要进行此类标准化,您需要注意布尔逻辑的规则,以及 != 和 IN 过滤条件实际上是如何实现的:
!=和IN运算符展开为其原始形式,其中!=变为检查属性是大于 (<) 还是小于 (>) 相应值,而IN变为检查属性是否等于 (==) 第一个值或第二个值或…一直到列表中的最后一个值为止。AND中嵌套OR等效于对原始AND运算对象应用多个AND的OR运算,并用一个OR运算对象取代原始OR。例如,AND(a, b, OR(c, d))等同于OR(AND(a, b, c), AND(a, b, d))。- 如果
AND的一个运算对象本身也是AND运算,则嵌套AND的运算对象可以汇入外层AND。例如,AND(a, b, AND(c, d))等同于AND(a, b, c, d)。 - 如果
OR的一个运算对象本身也是OR运算,则嵌套OR的运算对象可以汇入外层OR。例如,OR(a, b, OR(c, d))等同于OR(a, b, c, d)。
如果我们将这些转换分阶段应用于示例过滤条件,使用比 Python 更简单的表示法,则会得到如下内容:
- 对
IN���!=运算符使用规则 #1:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', OR(tags < 'perl', tags > 'perl')))) - 对嵌套在
AND中的最内层OR使用规则 #2:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', OR(AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl')))) - 对嵌套在另一个
OR中的OR使用规则 #4:AND(tags == 'python', OR(tags == 'ruby', tags == 'jruby', AND(tags == 'php', tags < 'perl'), AND(tags == 'php', tags > 'perl'))) - 对嵌套在
AND中的其余OR使用规则 #2:OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', AND(tags == 'php', tags < 'perl')), AND(tags == 'python', AND(tags == 'php', tags > 'perl')))
- 使用规则 #3 折叠其余的嵌套
AND:OR(AND(tags == 'python', tags == 'ruby'), AND(tags == 'python', tags == 'jruby'), AND(tags == 'python', tags == 'php', tags < 'perl'), AND(tags == 'python', tags == 'php', tags > 'perl'))
注意:对于某些过滤条件,这种标准化可能会导致组合爆炸。例如,假设 AND 有 3 个 OR 子句,每个子句有 2 个基本子句。进行标准化后,将变为有 8 个 AND 子句的 OR,每个子句有 3 个基本子句:即原来的 6 项变为了 24 项。
指定排序顺序
您可以使用 order() 方法指定查询返回结果的顺序。此方法采用参数列表,每个参数都是属性对象(按升序排序)或其否定(表示降序)。例如:
这将检索所有 Greeting 实体,并按其 content 属性的升序值进行排序。具有相同 content 属性的连续实体的运行将按其 date 属性的降序值进行排序��您可以使用多个 order() 调用来实现相同的效果:
注意:将过滤条件与 order() 组合使用时,Datastore 会拒绝某些组合。
特别是,当您使用不等式过滤条件时,第一个排序顺序(如果有)必须指定与过滤条件相同的属性。此外,有时您还需要配置二级索引。
祖先查询
祖先查询允许您对数据存储区进行高度一致性查询,但具有相同祖先的实体遵循每秒 1 次���入的���制。下面是使用数据存储区中的客户及其相关购买,对祖先查询和非祖先查询的权衡和结构进行的简单比较。
在以下非祖先示例中,数据存储区中有一个实体对应每个 Customer,另一个实体对应每个 Purchase,并且 KeyProperty 指向客户。
要查找属于该客户的所有购买,您可以使用以下查询:
在本示例中,数据存储区提供高写入吞吐量,但仅具有最终一致性。 如果添加了新的购买,您可能会收到过时的数据。您可以使用祖先查询避免此行为。
对于祖先查询的客户和购买,您仍然具有有两个单独实体的相同结构。客户部分是相同的。但是,在创建购买时,您不再需要为购买指定 KeyProperty()。这是因为如果您使用祖先查询,在您创建购买实体时会调用客户实体的键。
每个购买都有键,客户也有自己的键。但是,每个购买键中都会嵌套有 customer_entity 键。请注意,这将遵循每个祖先每秒写入一次的限制。 以下代码会创建具有一个祖先的实体:
要查询指定客户的购买,请使用以下查询。
查询特性 (attribute)
查询对象具有以下只读数据特性:
| 特性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| kind | str | None | 种类名称(通常是类名称) |
| ancestor | Key | None | 要查询的指定祖先 |
| filters | FilterNode | None | 过滤条件表达式 |
| orders | Order | None | 排序顺序 |
输出查询对象(或对其调用 str() 或 repr())会生成格式规范的字符串表示形式:
过滤结构化属性值
查询可以直接过滤结构化属性的字段值。
例如,检索地址中城市为 'Amsterdam' 的所有联系人的查询类似如下:
如果组合多个此类过滤条件,则过滤条件可以匹配同一 Contact 实体中的不同 Address 子实体。例如:
可以找到一个地址中城市为 'Amsterdam' 且另一个不同地址中街道为 'Spear St' 的联系人。但是,至少对于等式过滤条件,您可以创建仅返回单个子实体中具有多个值的结果的查询:
如果使用此方法,则在查询中将忽略等于 None 的子实体属性。如果属性具有默认值,则必须将其明确设置为 None 以在查询中忽略它,否则查询将包含一个过滤条件,要求该属性值等于默认值。例如,如果 Address 模型的属性 country 具有 default='us',则上述示例仅返回国家/地区等于'us' 的联系人。要考虑查询其他国家/地区值的联系人,您需要使用 Address(city='San Francisco', street='Spear St',
country=None) 进行过滤。
如果子实体的任何属性值等于 None,则会忽略这些属性值。因此,过滤子实体属性值 None 是没有意义的。
使用以字符串命名的属性
有时,您希望根据由字符串指定名称的属性对查询进行过滤或排序。例如,如果您允许用户输入像 tags:python 这样的搜索查询,则将其转换成类似如下的查询会比较方便:
Article.query(Article."tags" == "python") # does NOT work
如果模型是 Expando,则过滤条件可以使用 GenericProperty,Expando 类被用于动态属性:
如果您的模型不是 Expando,也可以使用 GenericProperty,但如果您想确保只使用已定义的属性名称,还可以使用 _properties 类特性
或者使用 getattr() 从类中获取:
区别在于 getattr() 使用属性的“Python 名称”,而 _properties 根据属性的“数据存储区名称”编入索引。这些区别仅在以如下方式声明属性时有所体现:
其中 Python 名称是 title,但数据存储区名称是t。
这些方法也适用于对查询结果进行排序:
查询迭代器
当查询正在进行时,其状态会保存在迭代器对象中。(大多数应用不会直接使用这些对象;调用 fetch(20) 通常比操作迭代器对象更直接。)获取此类对象有两种基本方法:
- 对
Query对象使用 Python 的内置iter()函数 - 调用
Query对象的iter()方法
第一种方法支持使用 Python for 循环(隐式调用 iter() 函数)来遍历查询。
第二种方法使用 Query 对象的 iter() 方法,这允许您将选项传递给迭代器以影响其行为。例如,要在 for 循环中使用仅限于键的查询,可以编写如下代码:
查询迭代器还有其他好用的方法:
| 方法 | 说明 |
|---|---|
__iter__()
| Python 的迭代器协议的一部分。 |
next()
| 返回下一个结果,如果没有,则引发 StopIteration 异常。 |
has_next()
| 如果是后续的 next() 调用将返回结果,则返回 True;如果将引发 StopIteration,则返回 False。在获知这个问题的答案之前阻止其他代码运行,并在用 next() 检索结果之前缓冲结果(如果有的话)。
|
probably_has_next()
| 与 has_next() 类似,但使用更快捷的方法,因而有时不准确。可能会返回假正例(当 next() 实际会引发 StopIteration 时为 True),但从不返回假负例(当 next() 实际会返回时为 False)。
|
cursor_before()
| 返回表示�����������返回的结果之前的点的查询游标。 如果没有可用的游标,则引发异常(特别是未传递 produce_cursors 查询选项的情况下)。 |
cursor_after()
| 返回表示紧邻上次返回的结果之后的点的查询游标。 如果没有可用的游标,则引发异常(特别是未传递 produce_cursors 查询选项的情况下)。 |
index_list()
| 返回已执行查询使用的索引列表,包括主索引、复合索引、种类索引和单属性索引。 |
查询游标
查询游标是表示查询中的恢复点的小型不透明数据结构。这对于向用户一次显示一页结果很有用;并且对于处理可能需要停止和恢复的长期作业也很有用。
查询游标的典型用法是使用查询的 fetch_page() 方法。
其工作原理类似于 fetch(),但返回一个三元组 (results, cursor, more)。
返回的 more 标志表示可能有更多结果;例如,用户界面可以使用该标志来禁用“下一页”按钮或链接。要请求后续页面,需将一个 fetch_page() 调用返回的游标传递给下一个调用。如果传入无效游标,则会引发 BadArgumentError。请注意,验证仅检查值是否采用 base64 编码。您将需要执行任何必需的进一步验证。
因此,要让用户查看与查询匹配的所有实体,且一次获取一页结果,您的代码可能如下所示:
...
请注意,此示例中使用 urlsafe() 和 Cursor(urlsafe=s) 来对游标进行序列化和反序列化。
这样,您便可以在响应一个请求时将游标传递给 Web 上的客户端,并在稍后的请求中从客户端接收回来。
注意:即使没有更多结果,fetch_page() 方法通常也会返回游标,但不能保证这种行为:返回的游标值可能为 None。另请注意,由于 more 标志是使用迭代器的 probably_has_next() 方法实现的,所以在极少数情况下,即使下一页是空的,也可能返回 True。
某些 NDB 查询不支持查询游标,但您可以修复此问题。
如果查询使用 IN、OR 或 !=,那么查询结果只有在按键排序的情况下才能使用游标。如果应用没有按键对结果进行排序并调用了 fetch_page(),则会收到 BadArgumentError。
如果
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name).fetch_page(N)
收到错误,请将其更改为
User.query(User.name.IN(['Joe', 'Jane'])).order(User.name, User.key).fetch_page(N)
您可以使用查询的 iter() 方法,而不是对查询结果“分页”来获取精确位置的游标。为此,请将 produce_cursors=True 传递给 iter();当迭代器在正确的位置时,调用其 cursor_after() 来获取紧邻其后的游标。(或者,类似地,调用 cursor_before() 获取紧邻其前的游标。)请注意,调用 cursor_after() 或 cursor_before() 可能会导致阻塞 Datastore 调用,重新运行部分查询以提取指向一组查询结果中间的游标。
要使用游标对查询结果进行反向分页,请创建反向查询:
为每个实体调用一个函数(“映射”)
假设您需要获取与查询返回的 Message 实体对应的 Account 实体。
您可以编写类似如下的代码:
但这种做法效率非常低:它等待获取一个实体,然后使用该实体;等待下一个实体,然后使用该实体。有大量时间用于等待。 另一种方法是编写一个映射到查询结果的回调函数:
此版本的运行速度比上面的简单 for 循环要快一些,因为允许一些并发操作。但是,因为 callback() 中的 get() 调用仍然是同步的,所以增益并不大。这种情况非常适合使用异步 get。
GQL
GQL 是一种类似于 SQL 的语言,用于从 App Engine Datastore 中检索实体或键。虽然 GQL 的功能与用于传统关系数据库的查询语言的功能不同,但 GQL 语法类似 SQL 的语法。GQL 参考中介绍了 GQL 语法。
您可以使用 GQL 构建查询。这��似于使用 Model.query() 创建查询,但使用 GQL 语法来定义查询过滤条件和顺序。要使用此方法,请注意:
ndb.gql(querystring)返回一个Query对象(与Model.query()返回的类型相同)。所有常用方法都可用于此类Query对象:fetch()、map_async()、filter()等。Model.gql(querystring)是ndb.gql("SELECT * FROM Model " + querystring)的简写。 通常,querystring 类似于"WHERE prop1 > 0 AND prop2 = TRUE"。- 要查询包含结构化属性的模型,可以在 GQL 语法中使用
foo.bar来引用子属性。 - GQL 支持类似 SQL 的参数绑定。应用可以定义查询,然后将值绑定到其中:或
调用查询的
bind()函数会返回一个新查询;这不会改变原始查询。 - 如果模型类重写
_get_kind()类方法,则 GQL 查询应使用该函数返回的种类,而不是类名称。 - 如果模型中的属性覆盖其名称(例如,
foo = StringProperty('bar')),则 GQL 查询应使用被覆盖的属性名称(在示例中为bar)。
如果查询中的某些值是用户提供的变量,请始终使用参数绑定功能。这可以避免基于语法入侵的攻击。
查询尚未导入的模型(或更广泛地说,尚未定义的模型)会引发错误。
使用未由模型类定义的属性名称会引发错误,除非该模型是 Expando。
为查询的 fetch() 指定的限制或偏移量将替换由 GQL 的 OFFSET 和 LIMIT 子句设置的限制或偏移量。不要将 GQL 的 OFFSET 和 LIMIT 与 fetch_page() 组合使用。请注意,App Engine 对查询施加的 1000 个结果的上限也适用于偏移量和限制。
如果您习惯使用 SQL,请注意使用 GQL 时的错误假设。 GQL 被转换为 NDB 的原生查询 API。 这与典型的对象关系映射器(如 SQLAlchemy 或 Django 的数据库支持)不同,在这种映射器中,API 调用���传输到数据库服务器之前会被转换为 SQL。GQL 不支持 Datastore 修改(插入、删除或更新),而只支持查询。