サイトのトップへ戻る

Google App Engine ドキュメント日本語訳

データストアを使用する

スケーラブルウェブアプリケーションにおいて、データの保存というのは難しい問題です。 ある時点においてユーザーは数あるウェブサーバの中の一つとやり取りをし、次にユーザーが送信したリクエストは前のリクエストとは違うサーバに送られることがあります。 全てのウェブサーバは、多くのマシン(おそらく世界中の異なる場所にある)にまたがっているデータとやり取りをする必要があります。

Google App Engineを使えば、何も気にする必要はありません。 簡単なAPIを使えば裏で App Engineのインフラが、配布や複製やデータの負荷分散といった全てのことを行ってくれます —そして強力なクエリとトランザクションも同様に使用できます。

App Engineのデータリポジトリである High Replication Datastore (HRD)Paxos アルゴリズムを使用して 複数のデータセンタにまたがってデータを複製します。データは、エンティティとして知られるオブジェクト内の、データストアに書き込まれます。 各エンティティは自身を唯一に識別するためのキーを持っています。 各エンティティは、必要に応じてとして指定できる別のエンティティを持っています; 持っている方のエンティティは、親エンティティのになります。 こうしてデータストア内のエンティティはファイルシステムのディレクトリ構造に似た階層構造空間を形成しています。 エンティティの親、親の親、などはそれぞれのアンセスターと呼びます。 エンティティの子、子の子、などはそれぞれのデセンダントと呼びます。 親のないエンティティは ルートエンティティです。

データストアは致命的な障害に対しては非常に耐性がありますが、データの一貫性保証に関してはあなたがこれまでに馴染んだものと異なっています。 エンティティが共通のアンセスターの子になるということは、同じエンティティグループに属するということです; 共通するアンセスターの持つキーをグループの親キーと呼び、エンティティグループを識別するのに使われます。 一つのエンティティグループに対するクエリーはアンセスタークエリーと呼ばれ、特定のエンティティキーではなく親キーを参照します。 エンティティグループは、一貫性とトランザクションの両方における最小単位になります: whereas queries over multiple entity groups may return stale, eventually consistent results, those limited to a single entity group always return up-to-date, strongly consistent results.

このガイドのサンプルアプリケーションでは関連するエンティティをエンティティグループに組み入れ、 一貫性の強い結果を返すためにこれらのエンティティグループにアンセスタークエリを使用しています。 サンプルコードのコメントでは、 we highlight some ways this approach might affect the design of your application. 詳細情報についてはStructuring Data for Strong Consistencyを参照してください。



データストアを使用した完全サンプル

Here is a new version of guestbook/guestbook.py that creates a page footer that stores greetings in the Datastore. The rest of this page discusses excerpts from this larger example, organized under the topics of storing the greetings and retrieving them.

guestbook.py
View on GitHub
import cgi
import urllib

from google.appengine.api import users
from google.appengine.ext import ndb

import webapp2

MAIN_PAGE_FOOTER_TEMPLATE = """    <form action="/sign?%s" 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="%s" name="guestbook_name">
      <input type="submit" value="switch">
    </form>
    <a href="%s">%s</a>
  </body>
</html>
"""

DEFAULT_GUESTBOOK_NAME = 'default_guestbook'

# We set a parent key on the 'Greetings' to ensure that they are all
# in the same entity group. Queries across the single entity group
# will be consistent.  However, the write rate should be limited to
# ~1/second.

def guestbook_key(guestbook_name=DEFAULT_GUESTBOOK_NAME):
    """Constructs a Datastore key for a Guestbook entity.

    We use guestbook_name as the key.
    """
    return ndb.Key('Guestbook', guestbook_name)


class Author(ndb.Model):
    """Sub model for representing an author."""
    identity = ndb.StringProperty(indexed=False)
    email = ndb.StringProperty(indexed=False)


class Greeting(ndb.Model):
    """A main model for representing an individual Guestbook entry."""
    author = ndb.StructuredProperty(Author)
    content = ndb.StringProperty(indexed=False)
    date = ndb.DateTimeProperty(auto_now_add=True)


class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)

        # Ancestor Queries, as shown here, are strongly consistent
        # with the High Replication Datastore. Queries that span
        # entity groups are eventually consistent. If we omitted the
        # ancestor from this query there would be a slight chance that
        # Greeting that had just been written would not show up in a
        # query.
        greetings_query = Greeting.query(
            ancestor=guestbook_key(guestbook_name)).order(-Greeting.date)
        greetings = greetings_query.fetch(10)

        user = users.get_current_user()
        for greeting in greetings:
            if greeting.author:
                author = greeting.author.email
                if user and user.user_id() == greeting.author.identity:
                    author += ' (You)'
                self.response.write('<b>%s</b> wrote:' % author)
            else:
                self.response.write('An anonymous person wrote:')
            self.response.write('<blockquote>%s</blockquote>' %
                                cgi.escape(greeting.content))

        if user:
            url = users.create_logout_url(self.request.uri)
            url_linktext = 'Logout'
        else:
            url = users.create_login_url(self.request.uri)
            url_linktext = 'Login'

        # Write the submission form and the footer of the page
        sign_query_params = urllib.urlencode({'guestbook_name':
                                              guestbook_name})
        self.response.write(MAIN_PAGE_FOOTER_TEMPLATE %
                            (sign_query_params, cgi.escape(guestbook_name),
                             url, url_linktext))

class Guestbook(webapp2.RequestHandler):
    def post(self):
        # We set the same parent key on the 'Greeting' to ensure each
        # Greeting is in the same entity group. Queries across the
        # single entity group will be consistent. However, the write
        # rate to a single entity group should be limited to
        # ~1/second.
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)
        greeting = Greeting(parent=guestbook_key(guestbook_name))

        if users.get_current_user():
            greeting.author = Author(
                    identity=users.get_current_user().user_id(),
                    email=users.get_current_user().email())

        greeting.content = self.request.get('content')
        greeting.put()

        query_params = {'guestbook_name': guestbook_name}
        self.redirect('/?' + urllib.urlencode(query_params))

app = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/sign', Guestbook),
], debug=True)

guestbook/guestbook.py の中身を上記に置き換え、ブラウザでhttp://localhost:8080/のページを再読み込みしてください。 いくつかメッセージを投稿し、メッセージが保存されて正しく表示されるか確認してください。

警告! ローカルのアプリケーションでクエリを実行すると、 App Engineはindex.yamlを作成したり更新したりします。 index.yamlが無かったり不完全な場合は、 アップロードしたアプリケーションが必要なインデックスが作成されていないクエリを実行した時にインデックスエラーが表示されます。 製品版でインデックスエラーを見逃さないようにするため、アプリケーションをアップロードする前には常にローカルで最低一回は新規クエリをテストしてください。 詳細情報についてはPython データストアのインデックス設定を参照してください。



送信された挨拶文を保存する

App Engine にはPython用のデータモデリングAPIが含まれています。 これはDjangoのデータモデリング APIに似ていますが、 裏ではApp Engineのスケーラブルデータストアを使用しています。

データモデリング APIを使用するために、サンプルでは google.appengine.ext.ndbモジュールをインポートしています:

guestbook.py
View on GitHub
from google.appengine.ext import ndb

ゲストブックアプリケーションの場合、ユーザーが投稿した挨拶文を保存したいですね。 各挨拶文には書いた人の名前、メッセージ文、メッセージが投稿された日時が含まれ、時系列順に表示することができます。 以下のコードは今回使用するデータモデルを定義しています。:

guestbook.py
View on GitHub
class Author(ndb.Model):
    """Sub model for representing an author."""
    identity = ndb.StringProperty(indexed=False)
    email = ndb.StringProperty(indexed=False)


class Greeting(ndb.Model):
    """A main model for representing an individual Guestbook entry."""
    author = ndb.StructuredProperty(Author)
    content = ndb.StringProperty(indexed=False)
    date = ndb.DateTimeProperty(auto_now_add=True)

上記は三つのプロパティを持つ Greeting モデルを定義しています: authorには、Eメールアドレスと書いた人のID(user_id)を持つ Authorオブジェクトが入ります。 contentには文字列が入ります。 dateにはdatetime.datetimeの値が入ります。

いくつかのプロパティのコンストラクタでは、挙動をより細かく設定するためにパラメータを指定します。 ndb.StringPropertyのコンストラクタにindexed=Falseパラメータを指定すると、このプロパティの値はインデックス化されません。 This saves us writes which aren't needed since we never use that property in a query. ndb.DateTimePropertyのコンストラクタにauto_now_add=Trueパラメータを指定すると、作成された時間のタイムススタンプdatetimeをオブジェクトに自動的に付与するようモデルを設定します, if the application doesn't otherwise provide a value. プロパティタイプとそれらのオプションの全一覧については、NDB プロパティを参照してください。

これで挨拶文用のデータモデルの準備ができたので、アプリケーションはこのモデルを使って新規にGreetingオブジェクトを作成してデータストアに格納できます。 Guestbook ハンドラで新規に挨拶文を作成してデータストアに保存しています:

guestbook.py
View on GitHub
class Guestbook(webapp2.RequestHandler):
    def post(self):
        # We set the same parent key on the 'Greeting' to ensure each
        # Greeting is in the same entity group. Queries across the
        # single entity group will be consistent. However, the write
        # rate to a single entity group should be limited to
        # ~1/second.
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)
        greeting = Greeting(parent=guestbook_key(guestbook_name))

        if users.get_current_user():
            greeting.author = Author(
                    identity=users.get_current_user().user_id(),
                    email=users.get_current_user().email())

        greeting.content = self.request.get('content')
        greeting.put()

        query_params = {'guestbook_name': guestbook_name}
        self.redirect('/?' + urllib.urlencode(query_params))

このGuestbook ハンドラでは新規に Greetingオブジェクトを作成し、ユーザーによって投稿されたデータをauthorプロパティとcontentプロパティに設定します。 Greetingの親は、Guestbook エンティティです。 There's no need to create the Guestbook entity before setting it to be the parent of another entity. In this example,the parent is used as a placeholder for transaction and consistency purposes. 詳細情報についてはトランザクションのページを参照してください。 共通のアンセスターを持つオブジェクトは同じエンティティグループに属します。 今回は dateプロパティを設定していないので、上記で設定したauto_now_add=Trueが使われ、dateにが現在の時間が自動的に設定されます。

最後に、 greeting.put() で新規オブジェクトをデータストアに保存しています。 このオブジェクトをクエリから既に取得している場合は、put()で既存のオブジェクトは更新されます。 今回はコンストラクタを使ってこのオブジェクトを新規作成しているので、データストアには新規のオブジェクトが追加されます。

High Replication Datastoreのクエリはエンティティグループ内でのみ強い一貫性が維持されるので、 この例では各挨拶文に同じ親を設定することで、全ての挨拶文を同じエンティティグループに割り当てています。 これは、ユーザーには書き込みをした直後に常に挨拶文が表示されるということです。 しかし、同一のテンティティグループに書き込める速度は1秒につき1回に制限されています。 実際にアプリケーションを設計する場合はこのことを覚えておいてください。 Memcacheなどのサービスを使用することで、 書き込み直後にエンティティグループをまたぐクエリをした場合に最新の結果が表示されない自体を軽減できます。



送信された挨拶文を取得する

App Engine データストアは、データモデルのための洗練されたクエリーエンジンを持っています。 App Engine データストアはこれまでのリレーショナルデータベースとは違うため、クエリをSQLでは指定できません。 代わりに、データは以下二つのやり方でクエリされます: データストアクエリ経由で、もしくはGQLと呼ばれるSQL-ライクのクエリ言語を使います。 データストアクエリの全ての機能にアクセスするには、GQLではなくDatastoreの使用を推奨します。

MainPage ハンドラは前回送信した挨拶文を取得して表示します。 データストアクエリは以下の部分で生成されます。:

guestbook.py
View on GitHub
greetings_query = Greeting.query(
    ancestor=guestbook_key(guestbook_name)).order(-Greeting.date)
greetings = greetings_query.fetch(10)


A Word About Datastore Indexes

Every query in the App Engine Datastore is computed from one or more indexes—tables that map ordered property values to entity keys. This is how App Engine is able to serve results quickly regardless of the size of your application's Datastore. Many queries can be computed from the builtin indexes, but for queries that are more complex the Datastore requires a custom index. Without a custom index, the Datastore can't execute these queries efficiently.

For example, our guest book application above filters by guestbook and orders by date, using an ancestor query and a sort order. This requires a custom index to be specified in your application's index.yaml file. You can edit this file manually or, as noted in the warning box earlier on this page, you can take care of it automatically by running the queries in your application locally. Once the index is defined in index.yaml, uploading your application will also upload your custom index information.

index.yamlファイルでのクエリーの定義は以下のようになります:

index.yaml
View on GitHub
indexes:
- kind: Greeting
  ancestor: yes
  properties:
  - name: date
    direction: desc

データストアインデックスに関しては全て データストアインデックスのページで読めます。 index.yamlファイルの正しい記述方法については Python データストアインデックスの設定で読めます。



次は...

これでゲストブックアプリケーションでは、Google アカウントを使ってユーザーを認証し、ユーザーがメッセージを送信し、他のユーザーが残したメッセージを表示することができるようになりました。 App Engineが自動的にスケーリング処理を行うため、アプリケーションを使う人が増えてもこのコードを修正をする必要はありません。

ここまで終わった段階では、HTMLのコンテンツとMainPageハンドラのコードが混在している状態です。 このままだとアプリケーションの外観を変更するのが難しく、特にアプリケーションが大きく複雑になるにつれてそれが顕著になるでしょう。 それでは、テンプレートを使用して外見を管理し、CSS スタイルシート用の静的ファイルを導入しましょう。

テンプレートを使用する >>