さんまがおいしい季節だねー(´・ω・`)

RDB ウェブアプリを Google App Engine へ移植する際の注意点とか

Google App Engine — タグ: — さくら @ 2009/09/04 22:24

Twit Delay も運用開始から一段落しまして、GAE での開発ノウハウも多少貯まってきたと思いますので、ちょろちょろ公開していきたいと思います。

で本日は、Google App Engine (GAE) で開発する際に一番問題…って言い方は微妙なんだけど、とりあえず生まれて始めてのプログラミングが GAE って人も稀だと思いますし、RDB を使ったウェブアプリの開発経験がある方が多いと思いますので、RDB から GAE のデータストアへ移植する際に把握しておいた方が良い点についてまとめてみました。

実際に Twit Delay は SQLite3 を使った RDB ウェブアプリを GAE に移植したのですが、その際にひっかかった、RDB と GAE データストアの違いはこんなところです。

  1. Create Table 等に相当する命令は無い
  2. Index は index.yaml で指定する
  3. COUNT(*) に相当する処理は無い
  4. トランザクションの単位はエンティティグループ
  5. データの一意性はキーで確保する
  6. バッチ処理はきつい

最初の二つはあまり大きな問題じゃない…訳でも無いけど、この辺理解しないとそもそもアプリ作れないはずなんでググるのマニュアル見てください。他の点についてもまあマニュアル見れば書いてるんですが、移植時に問題になることが多いと思いますので以下で説明したいと思います。

あ、以下の説明はすべて Python 版の GAE になってます。Java 版は使ったことありませんので説明してません。

COUNT(*) に相当する処理は無い

厳密に言うと COUNT(*) に相当する処理が無いんじゃなくて、COUNT(*) に相当する処理が RDB 的な意味では使えない感じです。マニュアルの GqlQueryQuery の count メソッドで説明されてますが、count() メソッドの返す件数の上限は 1000 です。

Twit Delay で当初、予約投稿数の上限を 850 にしていたのはこの制限のためだったりします。今上限 100 になってるのは、実際に運用してみてそんなに大量に予約する人がいなかったためです。

んで、件数が必要となるアプリの場合、COUNT(*) と対応する処理を自前で実装する必要があります。この実装にはトランザクション処理が必要ですので、具体例は次の項目をご覧ください。

トランザクションの単位はエンティティグループ

GAE のデータストアではトランザクションを指定できる単位がエンティティグループになってます。たぶん実例見てもらった方が早いと思いますので例示します。

とりあえず DB 関係の本でよく出てくる社員管理を例にします。

class Section(db.Model):
    name = db.StringProperty(required=True)
    employees = db.IntegerProperty(required=True, default=0)

class Employee(db.Model):
    name = db.StringProperty(required=True)

とりあえずこんな感じでデータストア定義されてるとして、えと、Section が課、Employee が社員です。
先ほど COUNT(*) の説明で述べたとおり、一応このアプリでは、課ごとの社員数を把握する必要があるという建前で、Section に課ごとの社員数を表す employees を持たせてます。まぁ一つの課に1000人以上社員が要る会社は無いような気もしますが、例なのでご容赦をw

このアプリで社員を追加するときは、以下のように Section を親とするエンティティグループを作成してトランザクションを実行します。

def create_employee(section_key, name):
    section = db.get(section_key)
    employee = Employee(parent=section, name=name)
    section.employees += 1
    db.put(employee)
    db.put(section)

query = db.Query(Section)
query.filter('name =', section_name)
section = query.fetch(1)[0]
db.run_in_transaction(create_employee, section.key(), name)

db.run_in_transaction がトランザクションを実行する関数です。Employee の parent 引数を指定することで、新規作成された Employee が section_key をキーとするエンティティグループの要素となります。section_name は CGI のパラメータ等で渡されているものと想像してください。

異なるエンティティグループに属するエンティティを、一度のトランザクションで処理することはできません。また、エンティティグループ一つあたりの最大データサイズが 1MB に制限されていますので、RDB アプリを単純に載せ替えようとする場合に問題になると思います。(というかなりましたw)

データの一意性はキーで確保する

本来エンティティグループより先に説明すべきだと思いますが、先にトランザクションから理解した方が話が早いと思いましたのでこの位置にしました。RDB で言うところの UNIQUE 制約に相当するものはデータストアにはありません。各エンティティの key_name を指定することで一意性は確保します。

上の例の Section が name に対して一意とするならば、エンティティ作成を以下のように行います。

section_name = ...
section = db.get_or_insert('key:%s' % section_name, name=section_name)

db.get_or_insert は、第一引数の key_name に対応するエンティティが存在すればそれを返し、存在しなければ作成してから返します。また、キーとエンティティ グループ に「key_name は先頭に数字を持つことができません。また、__*__(2 つのアンダースコアで始まり、終了する)の形式を取ることもできません。」と書かれていますので、key_name に ‘key:’ プレフィクスを付けてサニタイズしています。

なお、Section が親要素を持つ場合は、get_or_insert ではエンティティを作成することはできません。Section の親要素として部門を表す Sector が存在し、

class Sector(db.Model):
    name = db.StringProperty(required=True)

各 Section は Sector に対して一意である場合、Key.from_path を使用して Section の一意性を確保する必要があります。

def create_section(sector_name, section_name):
    sector_key_name = 'key:%s' % sector_name
    section_key_name = 'key:%s' % section_name
    section = db.get(db.Key.from_path('Sector', sector_key_name, 'Section', section_key_name))
    if section:
        return section
    sector = db.get(db.Key.from_path('Sector', sector_key_name))
    section = Section(key_name=section_key_name, parent=sector, name=section_name)
    db.put(section)
    return section

section = db.run_in_transaction(create_section, sector_name, section_name)

Key.from_path() はパスからキーを生成します。生成されたキーは各エンティティの key() メソッドが返す値と同じです。上の create_section では、Key.from_path() を使用してエンティティを作成する前にキーを生成し、そのキーを使って検索できたら新しいエンティティを作成せずそれを返し、検索できなかったら新しいエンティティを作成してそれを返しています。

データストアにあるのはエンティティの一意性のみで、RDB の一意性制約と直接的に対応する機構は無い(と思います)ので注意してください。

バッチ処理はきつい

GAE が直接制約を課している訳ではないのですが、いわゆるバッチ処理を実行すると管理パネルで実行時間の警告がよくでます。

Twit Delay ではクッキーの削除をバッチ処理でしています。最初同じ処理で OAuth Token の削除とかもしていたのですが、大したデータ量でも無いのに警告が頻発したため、(OAuth Token の退役を自動で行わないようにするため)Sign Out 処理を後から付け足しました。

RDB でよくある SELECT してループして hogefuga みたいな処理は同じように警告される可能性がありますので、可能ならオンライン処理で代替した方が良いと思います。

おまけ

んで今 GAE で動かしている Twit Delay ですが、最後のバッチ処理に関わる問題で、内部的に分散処理させるかサーバ移転しようか検討してたりします。バッチ処理で Twitter API を呼び出している関係上ある程度処理時間が必要となり元から警告が出やすい作りなのですが、この程度で警告出されるぐらいなら、さくらスタンダードの方がよっぽどマシのような気もしてたりします。

なんで GAE に不向きなアプリも存在するようですので、実用として使う場合は気をつけた方が良いんじゃないかなーと思います。

以上、RDB を Google App Engine へ移植する際の注意点とかでした。
間違い等ございましたら、コメントにてご指摘頂けると助かります。

0 Comments »

コメントはまだありません。

この投稿へのコメントの RSS フィード。 TrackBack URI

コメントする

Copyright © 2017 さくらたんどっとびーず | powered by WordPress with Barecity