Python 2 App Engine NDB クライアント ライブラリの概要

Google Datastore NDB クライアント ライブラリを使用すると、App Engine Python アプリから Datastore に接続できます。NDB クライアント ライブラリは従来の DB データストア ライブラリを基盤として、次のデータストア機能が追加されています。

  • エンティティでネスト構造を使用できるようにする StructuredProperty クラス。
  • 統合された自動キャッシュ。これは通常、インコンテキスト キャッシュや Memcache を介して高速かつ低負荷の読み取りを可能にします。
  • 同期 API に加えて、同時アクション用の非同期 API もまたサポートされます。

このページでは、App Engine NDB クライアント ライブラリの概要について説明します。Python 3 をサポートする Cloud NDB に移行する方法については、Cloud NDB への移行をご覧ください。

エンティティ、キー、プロパティの定義

Datastore には、「エンティティ」と呼ばれるデータ オブジェクトが格納されます。各エンティティには 1 つ以上の「プロパティ」があります。プロパティは、サポートされるいずれかのデータ型を持つ名前付きの値です。たとえば、文字列、整数、別のエンティティへの参照などをプロパティとして設定できます。

各エンティティは「キー」、つまりアプリケーション データストア内で一意の識別子によって区別されます。1 つのキーが別のキーの「親」となることがあります。場合によっては、親自体にも親があり、さらにその親そのまた親と続いていくこともあります。このような親子関係の最上位にある、親を持たないキーは、「ルート」と呼ばれます。

エンティティ グループ内のルート エンティティと子エンティティの間の関係を表示します。

同じルートに由来するキーを持つすべてのエンティティは、「エンティティ グループ」または「グループ」を形成します。互いに異なるグループに属する複数のエンティティを変更した場合、「無秩序」になるエラーが発生したように見えることがあります。アプリケーションのセマンティクスでこれらのエンティティが互いに無関係な場合は、問題ありません。しかし、いくつかのエンティティの変更に一貫性が必要な場合は、アプリケーションでこれらのエンティティを作成するときに同じグループに含めるべきです。

次に示すエンティティの関係図とコードサンプルでは、Guestbook の中に複数の Greetings が含まれ、それぞれに content および date プロパティが付属しています。

含まれているコードサンプルによって作成されたエンティティの関係を表示します。

この関係は、下記のコードサンプルとして実装されます。

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 エンティティには、挨拶(greeting)メッセージのテキスト コンテンツとメッセージの作成日の 2 つのプロパティがあります。

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() メソッドを呼び出します。

guestbook 内の挨拶メッセージが「無秩序」に見えないようにするために、アプリケーションは 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 エンティティを返すクエリが生成されます。また、クエリでは、エンティティ プロパティの値とキーに基づいてフィルタを指定することもできます。この例のように、クエリで祖先を指定して、特定の祖先に「属する」エンティティのみを見つけることができます。クエリでは、並べ替え順序を指定できます。特定のエンティティが、フィルタと並べ替え順序のすべてのプロパティに関して少なくとも 1 つの値(null も可能)を持ち、しかもプロパティ値がすべてのフィルタ基準を満たす場合には、そのエンティティが結果として返されます。

各クエリは 1 つのインデックス(つまりクエリ結果を適切な順序で並べたテーブル)を使用します。基盤となるデータストアは、単純なインデックス(1 つのプロパティのみを使用するインデックス)を自動的にいくつか保持します。

また、構成ファイル index.yaml 内に複雑なインデックスを定義します。開発用ウェブサーバーは、インデックスがまだ構成されていないクエリが検出されると、このファイルに推奨を自動的に追加します。

アプリケーションをアップロードする前にこのファイルを編集して、インデックスを手動で調整できます。gcloud app deploy index.yaml を実行することにより、アプリケーションのアップロードとは別個にインデックスを更新できます。データストアに大量のエンティティがある場合、新しいインデックスを作成するのに時間がかかります。この場合、新しいインデックスを使用するコードをアップロードする前に、インデックス定義を更新することをおすすめします。管理コンソールを使用すると、インデックスの作成がいつ終了したかを検知できます。

インデックス メカニズムはさまざまなクエリをサポートしているため、ほとんどのアプリケーションに適しています。ただし、他のデータベース テクノロジーで一般にサポートされているいくつかの種類のクエリが、サポートされていません。特に、結合がサポートされていません。

NDB の書き込みを理解する: commit、キャッシュの無効化、適用

NDB がデータを書き込む手順は次のとおりです。

  • commit フェーズで、基盤データストア サービスが変更を記録します。
  • 影響を受けるエンティティのキャッシュを NDB が無効にします。こうして、その後の読み取りではキャッシュから古い値を読み取るのではなく、基盤データストアから読み取ってキャッシュに入れます。
  • 最後に(おそらく数秒後に)、基盤データストアが変更を適用します。それによって、変更がグローバル クエリで認識でき、結果整合性のある読み取りを実現します。

データを書き込む NDB 関数(put() など)は、キャッシュが無効になった後に返されます。適用フェーズは非同期的に発生します。

commit フェーズで障害が発生すると自動的に再試行されますが、障害が続けて発生する場合はアプリケーションが例外を受け取ります。commit フェーズが成功して適用が失敗した場合、次のいずれかが発生すると、適用フェーズがロール フォワードされて完了します。

  • 定期的なデータストアの「スイープ」により、未完了の commit ジョブをチェックし、適用します。
  • 影響を受けるエンティティ グループ内で、それ以降に書き込み、トランザクション、または強い整合性のある読み取りが発生すると、読み込み、書き込み、またはトランザクションの前に未適用の変更が適用されます。

この動作は、アプリケーションに対してデータがいつ、どのように表示されるかに影響を与えます。NDB 関数が結果を返した後、数百ミリ秒ほどの間は、基盤データストアに対して変更が必ずしも完全に適用されない可能性があります。変更が適用されている最中に実行された非祖先クエリでは、非整合状態が見られることがあります(つまりすべての変更ではなく、一部の変更のみが適用された状態)。

トランザクションとデータのキャッシュ

NDB クライアント ライブラリは、複数のオペレーションを 1 回のトランザクションとしてグループ化できます。トランザクション内のすべてのオペレーションが成功しない限り、そのトランザクションは成功になりません。オペレーションが 1 つでも失敗すると、トランザクションは自動的にロールバックされます。この機能は、複数のユーザーが同じデータに同時にアクセスしたり操作したりする可能性がある分散型ウェブ アプリケーションの場合に特に役立ちます。

NDB は、データの「ホットスポット」用のキャッシュ サービスとして Memcache を使用します。アプリケーションがいくつかのエンティティを頻繁に読み取る場合、NDB はキャッシュからそれらを素早く読み取ることができます。

NDB での Django の使用

Django ウェブ フレームワークとともに NDB を使用するには、Django の settings.py ファイルの.MIDDLEWARE_CLASSES エントリに google.appengine.ext.ndb.django_middleware.NdbDjangoMiddleware を追加します。他のすべてのミドルウェア クラスの前にこれを挿入することをおすすめします。このミドルウェアより前に他のミドルウェアが起動される場合、そのミドルウェアがデータストアを呼び出すと、それが正しく処理されないためです。Django ミドルウェアの詳細をご確認ください。

次のステップ

以下の詳細を確認する。