Web アプリケーションを提供するには要求/応答モデルが効果的ですが、App Engine アプリケーションを開発またはサポートする場合は、このモデルが不向きな方法でデータストアを操作できると便利なことがよくあります。以前は、こういった操作には、app3 や App Rocket などの回避方法が必要でしたが、App Engine SDK リリース 1.1.9 からは、remote_api モジュール形式という新しい方法でデータストアと連携できるようになりました。このモジュールを使用すると、すでに知っている自分の好きな App Engine Apps の API を使用して、App Engine データストアにリモート アクセスできます。
この記事では、remote_api モジュールとその基本機能について説明するほか、アプリケーションのデータストアにアクセスするインタラクティブなコンソールを取得する方法を示します。そして、remote_api module モジュールの制限について簡単に説明し、最後に、高度な例を取り上げながら、マップ/削減操作の「マップ」部分を実装していきます。これにより、この種類のすべてのエンティティで関数を実行できるようになります。
remote_api モジュールは、「ハンドラ」と「スタブ」の 2 つの部分で構成されています。ハンドラは、サーバーにインストールして、データストア要求をリモートで処理します。一方、スタブは、クライアントにセットアップして、データストア要求をリモート ハンドラへの呼び出しに変換します。remote_api は、データストアの最下位レベルで動作するため、一度スタブをセットアップしたら、自分がリモート データストアで動作していることを気にする必要がなくなります: 注意事項はいくつかありますが、データストアに直接アクセスしているのとまったく同じように動作します。
ハンドラのインストールも簡単です。次の行を、「handler」キーの下の app.yaml に追加するだけでいいのです。
handlers:
- url: /remote_api
script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
login: admin
これにより、URL /remote_api の下に remote_api ハンドラがインストールされます。別のエンドポイントを選択することもできますが、下記の例を変更しなければなりません。ハンドラが「login: admin」を指定していることに注意してください。誰もがデータストアに自由にアクセスできないようにするには、これが非常に重要です。
app.yaml ファイルを更新したら、アプリケーションの appcfg.py update を実行し、新しいマッピングをアップロードする必要があります。
remote_api で行える最も簡単で便利な作業の 1 つに、本番データストアにアクセスできるインタラクティブな Python コンソールをセットアップするというものがあります。「appengine_console.py」という新しい Python ファイルを作成し、次のテキストを入力してください:
#!/usr/bin/python
import code
import getpass
import sys
sys.path.append("~/google_appengine")
from google.appengine.ext.remote_api import remote_api_stub
from google.appengine.ext import db
def auth_func():
return raw_input('Username:'), getpass.getpass('Password:')
if len(sys.argv) < 2:
print "Usage: %s app_id [host]" % (sys.argv[0],)
app_id = sys.argv[1]
if len(sys.argv) > 2:
host = sys.argv[2]
else:
host = '%s.appspot.com' % app_id
remote_api_stub.ConfigureRemoteDatastore(app_id, '/remote_api', auth_func, host)
code.interact('App Engine interactive console for %s' % (app_id,), None, locals())
このコードは非常にわかりやすく、あまり説明を要しません。6 行目では、SDK ディレクトリを Python パスに追加します。ここで示すコードは、Linux または OS X を使用しており、ホーム ディレクトリに SDK がインストールされている場合は適切に動作します。Windows では、これを、「c:\google_appengine」など、SDK がインストールされているパスに変更します。OS X でランチャーを使用している場合は、パスを「/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine」に設定する必要があります。
11 行目では、必要なときにユーザーから認証情報を取得するための関数を定義します。この例では、コンソールを介して認証情報を求めていますが、好きな方法を利用できます。14 行目からはアプリケーション ID と省略可能なホスト名をコマンド ラインから抽出し、22 行目では、remote_api で提供されたヘルパー関数を使用して環境をセットアップし、すべてのデータストア要求をリモート データストアに送信します。最後に、code.Interact を呼び出して、インタラクティブな Python インタープリタを開始します。
モデル定義など、自分のアプリケーションで定義されたモジュールにアクセスするには、そのアプリケーションが Python パスにあることを確認する必要があります。一番簡単な方法でこれを行うには、新しいコマンドを実行する前に、ディレクトリをアプリケーションのルート ディレクトリ(app.yaml が含まれるディレクトリ)に変更します。そして、次を実行します:
python appengine_console.py myapp
「myapp」を自分のアプリケーションのアプリケーション ID に置き換えると、Python のインタラクティブなコンソールのプロンプトが表示されます。
例を示すために、ここでは、Getting Started ドキュメントの Guestbook アプリケーションを使用します。Guestbook アプリケーションのルート ディレクトリにいる場合は、次を発行します:
>>> import helloworld >>> from google.appengine.ext import db
これで、Guestbook アプリケーションのコンテンツにアクセスできるので、サーバー上で実行するコードを記述するようにコマンドを発行できます:
>>> # Fetch the most recent 10 guestbook entries
>>> entries = helloworld.Greeting.all().order("-date").fetch(10)
>>>
>>> # Create our own guestbook entry
>>> helloworld.Greeting(content="A greeting").put()
通常、コンソールは、あなたがデータストアに直接アクセスしているかのように動作しますが、スクリプトがマシン上で実行されているため、実行時間を気にする必要はありません。また、ローカル マシンのすべてのファイルとリソースにも通常どおりアクセスできます。
remote_api モジュールは、ネイティブの App Engine データストアとまったく同じ動作を維持するために、可能な限りどんなことでも行います。これにより、効率が悪くなるような状況も発生します。remote_api を使用するときに、覚えておくべき事項をいくつか次に示します:
データストアには HTTP を介してアクセスするため、ローカルでアクセスするよりもオーバーヘッドや遅延が若干増えます。速度を上げて負荷を減らすには、get や put をバッチ処理し、エンティティをまとめてクエリからフェッチするようにして、ラウンド トリップの数を抑えるようにします。このアドバイスは、remote_api に限らず、データストアを通常どおり使用するときにも役立ちます。バッチ処理は、1 つのデータストア処理と見なされているからです。たとえば、次のコードをご覧ください:
for key in keys: rec = MyModel.get(key) rec.foo = bar rec.put()
上記のコードの代わりに、次のコードを実行します:
records = MyModel.get(keys) for rec in records: rec.foo = bar db.Put(records)
両方とも同じ働きをしますが、後者で必要なラウンドトリップは全体で 2 つである一方、前者ではエンティティごとに 2 つのラウンドトリップを必要とします。
remote_api は HTTP 上で動作するため、実行するすべてのデータストア呼び出しで、予想される通常のデータストア割り当て数のほかに、HTTP 要求(バイト単位の出入)の使用割り当て数が発生します。remote_api を使用して一括更新を行う場合はこのことを覚えておいてください。
ネイティブに実行する場合は、API 要求と応答の 1 MB 制限が引き続き適用されます。エンティティが特に大きい場合は、一度にフェッチまたは追加する数がこの制限を上回らないようにします。残念ながら、これはラウンド トリップ数を最小限に抑えるというアドバイスと矛盾します。そこで、要求または応答のサイズ制限を超えないようにしながら、可能な限り大きなバッチを使用することをおすすめします。ただし、エンティティのほとんどで、これが問題になることはほとんどありません。
データストア アクセスの一般的なパターンの一例を次に示します:
q = MyModel.all() for entity in q: # Do something with entity
これを行うとき、SDK は、データストアから 20 にまとめたバッチでエンティティをフェッチします。そして、既存のエンティティを使い果たした時点で新しいバッチをフェッチします。remote_api は、個別の要求で各バッチをフェッチしなければならないため、効率的ではありません。そこで、remote_api は、バッチごとに新しいクエリ全体を実行し、オフセット機能を使用して結果に進みます。
必要なエンティティ数がわかっていれば、必要な数だけ要求し、1 回の要求ですべてをフェッチします:
entities = MyModel.all().fetch(100) for entity in entities: # Do something with entity
必要なエンティティ数がわからない場合は、__key__ 疑似プロパティを使用して、大きな結果セットを効率的に反復処理します。これにより、標準のデータストア クエリの 1000 エンティティ制限を回避することもできます。
entities = MyModel.all().fetch(100)
while entities:
for entity in entities:
# Do something with entity
entities = MyModel.all().filter('__key__ >', entities[-1].key()).fetch(100)
remote_api でトランザクションを実装するために、トランザクションには、そのトランザクション内でフェッチされたエンティティに関する情報のほか、トランザクション内で追加または削除されたエンティティのコピーも蓄積されます。トランザクションがコミットされたら、その情報すべてを App Engine サーバーに送信し、そのサーバーからトランザクション内で使用されたすべてのエンティティを再度フェッチし、変更されていないトランザクションを確認し、トランザクションが行ったすべての変更を追加して削除し、コミットする必要があります。矛盾が生じたら、サーバーはトランザクションをロール バックして、クライアント エンドに通知します。そして、そのクライアントで、同じプロセスを最初からやり直さなければなりません。
このアプローチは正常に機能し、ローカル データストアのトランザクションで提供される機能を正確に再現しますが、効率的ではありません。もちろん、必要な場合はトランザクションを使用してください。ただし、効率性を考慮し、実行するトランザクションの数は抑え、できるだけ簡素化するようにしてください。
remote_api の機能とその制限について紹介したので、次は、これまで学んできた内容を実際のツールで使用します。指定した種類のすべてのエンティティを反復したり、そのデータを抽出したり、変更してから更新されたエンティティをデータストアに再度格納できると便利なことがよくあります。
これを実現するために、シンプルな「マップ」フレームワークを実装します。まず、クラス Mapper を定義します。このクラスはサブクラスを拡張する map() メソッドと、KIND と FILTERS の 2 つのフィールドを公開します。前者のフィールドは、マップする種類を、後者は適用する任意のフィルタを定義します。
class Mapper(object):
# Subclasses should replace this with a model class (eg, model.Person).
KIND = None
# Subclasses can replace this with a list of (property, value) tuples to filter by.
FILTERS = []
def map(self, entity):
"""Updates a single entity.
Implementers should return a tuple containing two iterables (to_update, to_delete).
"""
return ([], [])
def get_query(self):
"""Returns a query over the specified kind, with any appropriate filters applied."""
q = self.KIND.all()
for prop, value in self.FILTERS:
q.filter("%s =" % prop, value)
q.order("__key__")
return q
def run(self, batch_size=100):
"""Executes the map procedure over all matching entities."""
q = self.get_query()
entities = q.fetch(batch_size)
while entities:
to_put = []
to_delete = []
for entity in entities:
map_updates, map_deletes = self.map(entity):
to_put.extend(map_updates)
to_delete.extend(map_deletes)
if to_put:
db.put(to_put)
if to_delete:
db.delete(to_delete)
q = self.get_query()
q.filter("__key__ >", entities[-1].key())
entities = q.fetch(batch_size)
見てのとおり、特に難しいことはありません。まず、便利なメソッド get_query() を定義します。このメソッドは、クラス定義で指定された種類とフィルタに一致するクエリを、__key__ 疑似プロパティ順に返します。このメソッドは、オプションでサブクラスによってオーバーライドされ、たとえば、実行時にフィルタを変更できます(等価フィルタを使用し、クエリを __key__ 順に並び替えている場合)。次に、インスタンス メソッド run() を定義します。このメソッドは、一致するすべてのエンティティをバッチで反復処理し、処理ごとに個別の map() 関数を呼び出して、必要に応じてエンティティを更新または削除します。
Mapper に関する注意事項: マップ プロセスはデータストアのスナップショットからは動作しません。したがって、マッピングの条件を満たす新しいエンティティを map() から返す場合は、プロセス中に後でそのエンティティを map() 関数に渡すことがあります。これを行うかどうかは、現在のキーと比較して、キーがどこで並び替えられているかによります。一般に、同じタイプのエンティティを map() 関数で新しく作成する場合は、そのエンティティを繰り返し処理しないように、何らかの方法で新しいエンティティと元のエンティティと区別する必要があります。
このクラスを使用するために、map() 関数を実装するサブクラスを定義します。この例では、「Bar!」というフレーズを「foo」というフレーズが含まれる任意の guestbook エントリに追加します:
class GuestbookUpdater(Mapper):
KIND = "Greeting"
def map(self, entity):
if entity.content.lower().find('foo') != -1:
entity.content += ' Bar!'
return ([entity], [])
return ([], [])
次に、クラスをインスタンス化して、run() を呼び出します:
mapper = MyMapper() mapper.run()
これは簡単に試すことができます: 前にセットアップしたインタラクティブなコンソールにコードを入力すればいいのです。
最後に、簡単ですが実用的な例として、新しいフレームワークを便利に利用するためのコードを紹介します: これにより、指定した種類のすべてのエンティティを削除できます。
class MyModelDeleter(Mapper):
KIND = "MyModel"
def map(self, entity):
return ([], [entity])
簡単ですね。Mapper はインスタンス変数として常に KIND と FILTERS にアクセスするよう注意しています。そこで、これを一般化して、実行時に種類とフィルタを選択できるようにします:
class BulkDeleter(Mapper):
def __init__(self, kind, filters=None):
self.KIND = kind
if filters:
self.FILTERS = filters
def map(self, entity):
return ([], [entity])
もちろん、ここでは、remote_api と Mapper フレームワークで行えることのほんの一部を紹介したに過ぎません。何か新しい使用方法を思い付いた場合は、ぜひグループに投稿してくださるようお願いいたします。