スケーラブルウェブアプリケーションにおいて、データの保存というのは難しい問題です。 ある時点においてユーザーは数あるウェブサーバの中の一つとやり取りをし、次にユーザーが送信したリクエストは前のリクエストとは違うサーバに送られることがあります。 全てのウェブサーバは、多くのマシン(おそらく世界中の異なる場所にある)にまたがっているデータとやり取りをする必要があります。
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.
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
モジュールをインポートしています:
from google.appengine.ext import ndb
ゲストブックアプリケーションの場合、ユーザーが投稿した挨拶文を保存したいですね。 各挨拶文には書いた人の名前、メッセージ文、メッセージが投稿された日時が含まれ、時系列順に表示することができます。 以下のコードは今回使用するデータモデルを定義しています。:
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
ハンドラで新規に挨拶文を作成してデータストアに保存しています:
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
ハンドラは前回送信した挨拶文を取得して表示します。
データストアクエリは以下の部分で生成されます。:
greetings_query = Greeting.query( ancestor=guestbook_key(guestbook_name)).order(-Greeting.date) greetings = greetings_query.fetch(10)
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
ファイルでのクエリーの定義は以下のようになります:
indexes: - kind: Greeting ancestor: yes properties: - name: date direction: desc
データストアインデックスに関しては全て データストアインデックスのページで読めます。
index.yaml
ファイルの正しい記述方法については Python データストアインデックスの設定で読めます。
これでゲストブックアプリケーションでは、Google アカウントを使ってユーザーを認証し、ユーザーがメッセージを送信し、他のユーザーが残したメッセージを表示することができるようになりました。 App Engineが自動的にスケーリング処理を行うため、アプリケーションを使う人が増えてもこのコードを修正をする必要はありません。
ここまで終わった段階では、HTMLのコンテンツとMainPage
ハンドラのコードが混在している状態です。
このままだとアプリケーションの外観を変更するのが難しく、特にアプリケーションが大きく複雑になるにつれてそれが顕著になるでしょう。
それでは、テンプレートを使用して外見を管理し、CSS スタイルシート用の静的ファイルを導入しましょう。