NDB 查询

应用可以使用查询在 Datastore 中搜索与特定搜索条件(称为过滤条件)匹配的实体。

概览

应用可以使用查询在 Datastore 中搜索与特定搜索条件(称为过滤条件)匹配的实体。例如,跟踪多个留言板的应用可以使用查询从一个留言板中检索消息,并按日期排序:

from google.appengine.ext import ndb
...
class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)
...
class MainPage(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 20

    def get(self):
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key('Book', guestbook_name or '*notitle*')
        greetings = Greeting.query_book(ancestor_key).fetch(
            self.GREETINGS_PER_PAGE)

        self.response.out.write('<html><body>')

        for greeting in greetings:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write('</body></html>')

有些查询比其他查询更复杂一些;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 类:

class Account(ndb.Model):
    username = ndb.StringProperty()
    userid = ndb.IntegerProperty()
    email = ndb.StringProperty()

通常,您并不希望检索指定种类的所有实体,而只想要检索某属性具有特定值或值范围的那些实体。

属性对象重载了一些运算符,以返回可用于控制查询的过滤条件表达式:例如,要查找 userid 属性具有确切值 42 的所有 Account 实体,可以使用表达式

query = Account.query(Account.userid == 42)

(如果您确定只有一个 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])

要过滤不等式,可以使用如下语法:

query = Account.query(Account.userid >= 40)

这将查找 userid 属性大于或等于 40 的所有 Account 实体。

其中两个运算 != 和 IN 是作为其他运算的组合实现的,看起来有点奇怪,如 != 和 IN 中所述。

您可以指定多个过滤条件:

query = Account.query(Account.userid >= 40, Account.userid < 50)

此示例组合了指定的过滤条件参数,返回 userid 值大于或等于 40 且小于 50 的所有 Account 实体。

注意:如前所述,Datastore 拒绝对多个属性使用不等式过滤的查询。

您可能会发现,分步构建查询过滤条件,比在单个表达式中指定整个查询过滤条件更方便,例如:

query1 = Account.query()  # Retrieve all Account entitites
query2 = query1.filter(Account.userid >= 40)  # Filter on userid >= 40
query3 = query2.filter(Account.userid < 50)  # Filter on userid < 50 too

query3 等同于前一个示例中的 query 变量。请注意,查询对象是不可变的,因此 query2 的构造不会影响 query1,而 query3 的构造不会影响 query1query2

!= 和 IN 运算

调用 NDB 属性中的 Article 类:

class Article(ndb.Model):
    title = ndb.StringProperty()
    stars = ndb.IntegerProperty()
    tags = ndb.StringProperty(repeated=True)

!=(不等于)和 IN(成员资格)运算是通过使用 OR 运算组合其他过滤条件来实现的。前者

property != value

实现为

(property < value) OR (property > value)

例如:

query = Article.query(Article.tags != 'perl')

等效于

query = Article.query(ndb.OR(Article.tags < 'perl',
                             Article.tags > 'perl'))

注意:您也许感到意外,但此查询搜索的并非是不包含“perl”作为标记的 Article 实体!相反,它会找出至少有一个标记不等于“perl”的所有实体。 例如,以下实体虽然将“perl”作为其标记之一,但也将包含在结果中:

Article(title='Perl + Python = Parrot',
        stars=5,
        tags=['python', 'perl'])

但是,以下实体不包括在内:

Article(title='Introduction to Perl',
        stars=3,
        tags=['perl'])

无法查询不包含等于“perl”的标记的实体。

同样,用于测试是否属于可能值列表中的成员之一的 IN 运算

property IN [value1, value2, ...]

实现为

(property == value1) OR (property == value2) OR ...

例如:

query = Article.query(Article.tags.IN(['python', 'ruby', 'php']))

等效于

query = Article.query(ndb.OR(Article.tags == 'python',
                             Article.tags == 'ruby',
                             Article.tags == 'php'))

注意:使用 OR 的查询会删除其结果中重复的数据:结果流不会多次包含同一个实体,即使实体与两个或更多子查询匹配也是如此。

查询重复属性

前面部分中定义的 Article 类也可以作为查询重复属性的示例。值得注意的是,类似于

的过滤条件使用单个值,虽然 Article.tags 是重复属性。您无法将重复属性与列表对象进行比较(Datastore 无法理解),并且类似于

Article.tags.IN(['python', 'ruby', 'php'])

的过滤条件,执行的操作与搜索标记值是列表 ['python', 'ruby', 'php']Article 实体完全不同:该过滤条件会搜索 tags 值(视为列表)中至少包含以上值之一的实体

对重复属性查询 None 值会产生未知行为,我们不建议这么做。

组合 AND 和 OR 运算

您可以任意嵌套 ANDOR 运算。例如:

query = Article.query(ndb.AND(Article.tags == 'python',
                              ndb.OR(Article.tags.IN(['ruby', 'jruby']),
                                     ndb.AND(Article.tags == 'php',
                                             Article.tags != 'perl'))))

但是,由于 OR 的实现方法,如果此形式的查询过于复杂,就可能失败并引发异常。更安全的做法是对这些过滤条件进行标准化,使得在表达式树的顶部(最多)只有一个 OR 运算,并且在此之下只有一级 AND 运算。

要进行此类标准化,您需要注意布尔逻辑的规则,以及 !=IN 过滤条件实际上是如何实现的:

  1. !=IN 运算符展开为其原始形式,其中 != 变为检查属性是大于 (<) 还是小于 (>) 相应值,而 IN 变为检查属性是否等于 (==) 第一个值或第二个值或…一直到列表中的最后一个值为止。
  2. AND 中嵌套 OR 等效于对原始 AND 运算对象应用多个 ANDOR 运算,并用一个 OR 运算对象取代原始 OR。例如,AND(a, b, OR(c, d)) 等同于 OR(AND(a, b, c), AND(a, b, d))
  3. 如果 AND 的一个运算对象本身也是 AND 运算,则嵌套 AND 的运算对象可以汇入外层 AND。例如,AND(a, b, AND(c, d)) 等同于 AND(a, b, c, d)
  4. 如果 OR 的一个运算对象本身也是 OR 运算,则嵌套 OR 的运算对象可以汇入外层 OR。例如,OR(a, b, OR(c, d)) 等同于 OR(a, b, c, d)

如果我们将这些转换分阶段应用于示例过滤条件,使用比 Python 更简单的表示法,则会得到如下内容:

  1. IN!= 运算符使用规则 #1:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php',
             OR(tags < 'perl', tags > 'perl'))))
  2. 对嵌套在 AND 中的最内层 OR 使用规则 #2:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         OR(AND(tags == 'php', tags < 'perl'),
            AND(tags == 'php', tags > 'perl'))))
  3. 对嵌套在另一个 OR 中的 OR 使用规则 #4:
    AND(tags == 'python',
      OR(tags == 'ruby',
         tags == 'jruby',
         AND(tags == 'php', tags < 'perl'),
         AND(tags == 'php', tags > 'perl')))
  4. 对嵌套在 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')))
  5. 使用规则 #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() 方法指定查询返回结果的顺序。此方法采用参数列表,每个参数都是属性对象(按升序排序)或其否定(表示降序)。例如:

query = Greeting.query().order(Greeting.content, -Greeting.date)

这将检索所有 Greeting 实体,并按其 content 属性的升序值进行排序。具有相同 content 属性的连续实体的运行将按其 date 属性的降序值进行排序。您可以使用多个 order() 调用来实现相同的效果:

query = Greeting.query().order(Greeting.content).order(-Greeting.date)

注意:将过滤条件与 order() 组合使用时,Datastore 会拒绝某些组合。 特别是,当您使用不等式过滤条件时,第一个排序顺序(如果有)必须指定与过滤条件相同的属性。 此外,有时您还需要配置二级索引。

祖先查询

祖先查询允许您对数据存储区进行高度一致性查询,但具有相同祖先的实体遵循每秒 1 次写入的限制。下面是使用数据存储区中的客户及其相关购买,对祖先查询和非祖先查询的权衡和结构进行的简单比较。

在以下非祖先示例中,数据存储区中有一个实体对应每个 Customer,另一个实体对应每个 Purchase,并且 KeyProperty 指向客户。

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    customer = ndb.KeyProperty(kind=Customer)
    price = ndb.IntegerProperty()

要查找属于该客户的所有购买,您可以使用以下查询:

purchases = Purchase.query(
    Purchase.customer == customer_entity.key).fetch()

在本示例中,数据存储区提供高写入吞吐量,但仅具有最终一致性。 如果添加了新的购买,您可能会收到过时的数据。您可以使用祖先查询避免此行为。

对于祖先查询的客户和购买,您仍然具有有两个单独实体的相同结构。客户部分是相同的。但是,在创建购买时,您不再需要为购买指定 KeyProperty()。这是因为如果您使用祖先查询,在您创建购买实体时会调用客户实体的键。

class Customer(ndb.Model):
    name = ndb.StringProperty()

class Purchase(ndb.Model):
    price = ndb.IntegerProperty()

每个购买都有键,客户也有自己的键。但是,每个购买键中都会嵌套有 customer_entity 键。请注意,这将遵循每个祖先每秒写入一次的限制。 以下代码会创建具有一个祖先的实体:

purchase = Purchase(parent=customer_entity.key)

要查询指定客户的购买,请使用以下查询。

purchases = Purchase.query(ancestor=customer_entity.key).fetch()

查询特性 (attribute)

查询对象具有以下只读数据特性:

特性类型默认值说明
kind str None 种类名称(通常是类名称)
ancestor Key None 要查询的指定祖先
filters FilterNode None 过滤条件表达式
orders Order None 排序顺序

输出查询对象(或对其调用 str()repr())会生成格式规范的字符串表示形式:

print(Employee.query())
# -> Query(kind='Employee')
print(Employee.query(ancestor=ndb.Key(Manager, 1)))
# -> Query(kind='Employee', ancestor=Key('Manager', 1))

过滤结构化属性值

查询可以直接过滤结构化属性的字段值。 例如,检索地址中城市为 'Amsterdam' 的所有联系人的查询类似如下:

query = Contact.query(Contact.addresses.city == 'Amsterdam')

如果组合多个此类过滤条件,则过滤条件可以匹配同一 Contact 实体中的不同 Address 子实体。例如:

query = Contact.query(Contact.addresses.city == 'Amsterdam',  # Beware!
                      Contact.addresses.street == 'Spear St')

可以找到一个地址中城市为 'Amsterdam' 且另一个不同地址中街道为 'Spear St' 的联系人。但是,至少对于等式过滤条件,您可以创建仅返回单个子实体中具有多个值的结果的查询:

query = Contact.query(Contact.addresses == Address(city='San Francisco',
                                                   street='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,则过滤条件可以使用 GenericPropertyExpando 类被用于动态属性:

property_to_query = 'location'
query = FlexEmployee.query(ndb.GenericProperty(property_to_query) == 'SF')

如果您的模型不是 Expando,也可以使用 GenericProperty,但如果您想确保只使用已定义的属性名称,还可以使用 _properties 类特性

query = Article.query(Article._properties[keyword] == value)

或者使用 getattr() 从类中获取:

query = Article.query(getattr(Article, keyword) == value)

区别在于 getattr() 使用属性的“Python 名称”,而 _properties 根据属性的“数据存储区名称”编入索引。这些区别仅在以如下方式声明属性时有所体现:

class ArticleWithDifferentDatastoreName(ndb.Model):
    title = ndb.StringProperty('t')

其中 Python 名称是 title,但数据存储区名称是t

这些方法也适用于对查询结果进行排序:

expando_query = FlexEmployee.query().order(ndb.GenericProperty('location'))

property_query = Article.query().order(Article._properties[keyword])

查询迭代器

当查询正在进行时,其状态会保存在迭代器对象中。(大多数应用不会直接使用这些对象;调用 fetch(20) 通常比操作迭代器对象更直接。)获取此类对象有两种基本方法:

  • Query 对象使用 Python 的内置 iter() 函数
  • 调用 Query 对象的 iter() 方法

第一种方法支持使用 Python for 循环(隐式调用 iter() 函数)来遍历查询。

for greeting in greetings:
    self.response.out.write(
        '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

第二种方法使用 Query 对象的 iter() 方法,这允许您将选项传递给迭代器以影响其行为。例如,要在 for 循环中使用仅限于键的查询,可以编写如下代码:

for key in query.iter(keys_only=True):
    print(key)

查询迭代器还有其他好用的方法:

方法说明
__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 编码。您将需要执行任何必需的进一步验证。

因此,要让用户查看与查询匹配的所有实体,且一次获取一页结果,您的代码可能如下所示:

from google.appengine.datastore.datastore_query import Cursor
...
class List(webapp2.RequestHandler):
    GREETINGS_PER_PAGE = 10

    def get(self):
        """Handles requests like /list?cursor=1234567."""
        cursor = Cursor(urlsafe=self.request.get('cursor'))
        greets, next_cursor, more = Greeting.query().fetch_page(
            self.GREETINGS_PER_PAGE, start_cursor=cursor)

        self.response.out.write('<html><body>')

        for greeting in greets:
            self.response.out.write(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        if more and next_cursor:
            self.response.out.write('<a href="/list?cursor=%s">More...</a>' %
                                    next_cursor.urlsafe())

        self.response.out.write('</body></html>')

请注意,此示例中使用 urlsafe()Cursor(urlsafe=s) 来对游标进行序列化和反序列化。 这样,您便可以在响应一个请求时将游标传递给 Web 上的客户端,并在稍后的请求中从客户端接收回来。

注意:即使没有更多结果,fetch_page() 方法通常也会返回游标,但不能保证这种行为:返回的游标值可能为 None。另请注意,由于 more 标志是使用迭代器的 probably_has_next() 方法实现的,所以在极少数情况下,即使下一页是空的,也可能返回 True

某些 NDB 查询不支持查询游标,但您可以修复此问题。 如果查询使用 INOR!=,那么查询结果只有在按键排序的情况下才能使用游标。如果应用没有按键对结果进行排序并调用了 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 调用,重新运行部分查询以提取指向一组查询结果中间的游标。

要使用游标对查询结果进行反向分页,请创建反向查询:

# Set up.
q = Bar.query()
q_forward = q.order(Bar.key)
q_reverse = q.order(-Bar.key)

# Fetch a page going forward.
bars, cursor, more = q_forward.fetch_page(10)

# Fetch the same page going backward.
r_bars, r_cursor, r_more = q_reverse.fetch_page(10, start_cursor=cursor)

为每个实体调用一个函数(“映射”)

假设您需要获取与查询返回的 Message 实体对应的 Account 实体。 您可以编写类似如下的代码:

message_account_pairs = []
for message in message_query:
    key = ndb.Key('Account', message.userid)
    account = key.get()
    message_account_pairs.append((message, account))

但这种做法效率非常低:它等待获取一个实体,然后使用该实体;等待下一个实体,然后使用该实体。有大量时间用于等待。 另一种方法是编写一个映射到查询结果的回调函数:

def callback(message):
    key = ndb.Key('Account', message.userid)
    account = key.get()
    return message, account

message_account_pairs = message_query.map(callback)
# Now message_account_pairs is a list of (message, account) tuples.

此版本的运行速度比上面的简单 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 的参数绑定。应用可以定义查询,然后将值绑定到其中:
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1")
    query2 = query.bind(3)
    
    query = ndb.gql("SELECT * FROM Article WHERE stars > :1", 3)

    调用查询的 bind() 函数会返回一个新查询;这不会改变原始查询。

  • 如果模型类重写 _get_kind() 类方法,则 GQL 查询应使用该函数返回的种类,而不是类名称。
  • 如果模型中的属性覆盖其名称(例如, foo = StringProperty('bar')),则 GQL 查询应使用被覆盖的属性名称(在示例中为 bar)。

如果查询中的某些值是用户提供的变量,请始终使用参数绑定功能。这可以避免基于语法入侵的攻击。

查询尚未导入的模型(或更广泛地说,尚未定义的模型)会引发错误。

使用未由模型类定义的属性名称会引发错误,除非该模型是 Expando。

为查询的 fetch() 指定的限制或偏移量将替换由 GQL 的 OFFSETLIMIT 子句设置的限制或偏移量。不要将 GQL 的 OFFSETLIMITfetch_page() 组合使用。请注意,App Engine 对查询施加的 1000 个结果的上限也适用于偏移量和限制。

如果您习惯使用 SQL,请注意使用 GQL 时的错误假设。 GQL 被转换为 NDB 的原生查询 API。 这与典型的对象关系映射器(如 SQLAlchemy 或 Django 的数据库支持)不同,在这种映射器中,API 调用在传输到数据库服务器之前会被转换为 SQL。GQL 不支持 Datastore 修改(插入、删除或更新),而只支持查询。