Python 2 NDB 客户端库概览

Google Datastore NDB 客户端库可让 App Engine Python 应用连接到 Datastore。NDB 客户端库基于旧版 DB Datastore库构建,并增添了以下数据存储功能:

  • 允许实体具有嵌套结构的 StructuredProperty 类。
  • 集成的自动缓存,通常通过上下文缓存和 Memcache 提供快速且所需资源少的读取。
  • 除了同步 API 之外,还支持用于执行并发操作的异步 API。

本页面简要介绍了 NDB 客户端库。

定义实体、键和属性

Datastore 将数据对象存储为实体。 每个实体都有一个或多个属性(多种受支持数据类型之一的指定值)。例如,属性可以是字符串、整数或对另一个实体的引用。

每个实体都由键进行标识,键是应用数据存储区中的唯一标识符。键可能具有父类,父类属于不同的键。 父类也可能有自己的父类,依此类推;在这个“链”的顶部的键不含父类,称为“根”

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

如果实体的键具有相同的根,则这些实体组成实体组或组。如果实体位于不同的组中,那么对这些实体进行的更改有时可能会“无序”发生。实体与应用的语义不相关时也不会受到影响。但是,如果对某些实体进行的更改需要保持一致,那么您的应用要在同个组中创建这些实体。

以下实体关系图和代码示例展示了 Guestbook 具有多个 Greetings,每个此类实体都具有 contentdate 属性。

显示包含的代码示例创建的实体关系

下方的代码示例实现了此关系。

import cgi
import textwrap
import urllib

from google.appengine.ext import ndb

import webapp2

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):
    def get(self):
        self.response.out.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*")
        greetings = Greeting.query_book(ancestor_key).fetch(20)

        greeting_blockquotes = []
        for greeting in greetings:
            greeting_blockquotes.append(
                '<blockquote>%s</blockquote>' % cgi.escape(greeting.content))

        self.response.out.write(textwrap.dedent("""\
            <html>
              <body>
                {blockquotes}
                <form action="/sign?{sign}" method="post">
                  <div>
                    <textarea name="content" rows="3" cols="60">
                    </textarea>
                  </div>
                  <div>
                    <input type="submit" value="Sign Guestbook">
                  </div>
                </form>
                <hr>
                <form>
                  Guestbook name:
                    <input value="{guestbook_name}" name="guestbook_name">
                    <input type="submit" value="switch">
                </form>
              </body>
            </html>""").format(
                blockquotes='\n'.join(greeting_blockquotes),
                sign=urllib.urlencode({'guestbook_name': guestbook_name}),
                guestbook_name=cgi.escape(guestbook_name)))

class SubmitForm(webapp2.RequestHandler):
    def post(self):
        # We set the parent key on each 'Greeting' to ensure each guestbook's
        # greetings are in the same entity group.
        guestbook_name = self.request.get('guestbook_name')
        greeting = Greeting(parent=ndb.Key("Book",
                                           guestbook_name or "*notitle*"),
                            content=self.request.get('content'))
        greeting.put()
        self.redirect('/?' + urllib.urlencode(
            {'guestbook_name': guestbook_name}))

app = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/sign', SubmitForm)
])

使用模型存储数据

模型是描述实体类型的类,包括实体属性的类型及其配置。模型与 SQL 中的表大致类似。您可以通过调用模型的类构造函数创建实体,然后通过调用 put() 方法进行存储。

以下示例代码定义了模型类 Greeting。每个 Greeting 实体都有两个属性:问候语的文本内容和问候语的创建日期。

class Greeting(ndb.Model):
    """Models an individual Guestbook entry with content and date."""
    content = ndb.StringProperty()
    date = ndb.DateTimeProperty(auto_now_add=True)
class SubmitForm(webapp2.RequestHandler):
    def post(self):
        # We set the parent key on each 'Greeting' to ensure each guestbook's
        # greetings are in the same entity group.
        guestbook_name = self.request.get('guestbook_name')
        greeting = Greeting(parent=ndb.Key("Book",
                                           guestbook_name or "*notitle*"),
                            content=self.request.get('content'))
        greeting.put()

要创建和存储新的问候语,应用将创建新的 Greeting 对象并调用其 put() 方法。

为确保留言板中的问候语不会出现“乱序”,应用会在创建新的 Greeting 时设置父键。因此,新问候语将与同一留言簿中的其他问候语将处在同一实体组中。 究其原因,应用使用执行查询时会使用祖先查询。

查询和索引

应用可以执行查询,以查找与特定过滤条件匹配的实体。

    @classmethod
    def query_book(cls, ancestor_key):
        return cls.query(ancestor=ancestor_key).order(-cls.date)

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.out.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name')
        ancestor_key = ndb.Key("Book", guestbook_name or "*notitle*")
        greetings = Greeting.query_book(ancestor_key).fetch(20)

典型的 NDB 查询按类型过滤实体。在此示例中,query_book 将生成一个返回 Greeting 实体的查询。查询还可以指定针对实体属性值和键的过滤条件。 在此示例中,查询可以指定祖先实体,仅查找“属于”特定祖先实体的实体。 查询可以指定排序顺序。如果给定实体针对过滤条件和排序顺序中的每个属性至少拥有一个值(可能为 null),并且属性值满足所有过滤条件,则该实体会作为结果返回。

每个查询都使用索引,即包含以指定顺序排列的查询结果的表。底层数据存储区自动维护简单索引(仅使用一个属性的索引)。

它在配置文件 index.yaml 中定义其复杂索引。开发网络服务器遇到尚未配置索引的查询时,会自动向该文件添加建议。

在上传应用之前,您可以通过修改该文件来手动优化索引。您可以通过运行 gcloud app deploy index.yaml 来更新索引,而不必上传应用。如果您的数据存储区拥有许多实体,为它们创建新索引需要花费很长时间;在这种情况下,您可以先更新索引定义,然后在上传使用相关新索引的代码。 您可以使用管理控制台查找索引完成构建的时间。

这种索引机制支持各种查询,适用于大多数应用。不过,它不支持其他数据库技术中的部分常见查询。具体来说,不支持连接。

了解 NDB 写操作:提交、使缓存失效及应用

NDB 按以下步骤写入数据:

  • 在提交阶段,底层数据存储区服务会记录更改。
  • NDB 使其受影响的实体/实体的缓存失效。因此,将来的读取操作将从底层数据存储区进行读取(并缓存),而不是从缓存中读取过时的值。
  • 最后,或许数秒之后,底层数据存储区将应用更改。它在全局查询和最终一致性读取中体现此更改。

写入数据的 NDB 函数(例如 put())将在缓存失效后返回;应用阶段将异步发生。

如果在提交阶段执行发生失败,则会自动重试,但如果重试之后仍然不能成功,则应用会收到异常。如果提交阶段成功但应用失败,则在发生以下任一情况时,应用将前滚至完成:

  • 数据存储区定期的“清除”会检查未完成的提交作业并应用它们。
  • 受影响的实体组中的下一个写入、事务或高度一致性读取会导致在读取、写入或事务之前应用尚未应用的更改。

此行为会影响数据对应用可见的方式和时间。在 NDB 函数返回结果后的几百毫秒的时间里,此更改可能不会完全应用于底层数据存储区。应用更改时执行的非祖先实体查询可能会导致不一致的状态,即更改的一部分而非全部。

事务和缓存数据

NDB 客户端库可将多个操作进行组成单个事务。只有事务中的每个操作都成功执行,事务才能成功;如果任何操作执行失败,该事务将被会自动回滚。这对于分布式网络应用尤其有用,此类应用可允许多个用户同时访问或操作相同的数据。

NDB 使用 Memcache 作为数据中缓存服务的“热点”。如果应用经常读取某些实体,NDB 可以从缓存中快速对其进行读取。

将 Django 与 NDB 搭配使用

要将 NDB 与 Django 网络框架一起使用,请将 google.appengine.ext.ndb.django_middleware.NdbDjangoMiddleware 添加到 Django settings.py文件中的 MIDDLEWARE_CLASSES 条目。您最好将其插入到任何其他中间件类前面,因为某些其他中间件可能会进行数据存储区调用,如果其他中间件的调用先于上述中间件,则后者将不会得到正确处理。如需了解详情,请参阅 Django 中间件

后续事项

详细了解以下内容: