Memcache API

http://code.google.com/appengine/docs/memcache/

はじめに

Memcacheサービスは君のアプリケーションに複数のアプリケーションからアクセス可能でハイパフォーマンスなオンメモリkey-valueキャッシュを提供してくれる。Memcacheは一時データのようにデータストアのような永続性やトランザクションが不要なデータや、高速アクセスのためにデータストアからコピーしておくようなデータに向いている。Memcache APIはDanga Interactiveのmemcachedと同様の機能を備え、互換性も保っている。

Memcache APIを利用すると次の理由からアプリケーションのパフォーマンスがあがり、データストアの負荷が軽減される:

  • データストアへのクエリが劇的に減少する
  • とても人気のあるページでもデータストア割り当ての使用量を削減する
  • 高負荷なクエリや操作の結果をキャッシュする
  • 一時的なカウンタの使用を可能にする

Memcache APIを使うとアプリケーションで一貫性のあるキャッシュを作成できる。キャッシュはアプリケーションのすべてのインスタンスから利用でき、メモリの圧力によって(つまり、キャッシュにたくさんのデータが溜まりすぎたとき)、もしくは開発者によって設定されたポリシーがあればそれに従ってそのデータは破棄される。キャッシュのポリシーはキャッシュ内に保持されるそれぞれのkey-valueペアに対して設定できる。また、キャッシュをすべて破棄したり、データの一部に有効期限を設定することも可能だ。

from google.appengine.api import memcache

# キャッシュに存在しなければ値を追加。1時間でキャッシュは破棄される
memcache.add(key="weather_USA_98105", value="raining", time=3600)

# いくつかの値を設定。これらのキーに対応する値がすでに存在するときは上書きされる
memcache.set_multi({ "USA_98105": "raining",
                     "USA_94105": "foggy",
                     "USA_94043": "sunny" },
                     key_prefix="weather_", time=3600)

# 整数値をアトミックにインクリメントする
memcache.set(key="counter", 0)
memcache.incr("counter")
memcache.incr("counter")
memcache.incr("counter")

Memcacheを使う

Memcacheはハイパフォーマンスな分散メモリオブジェクトキャッシュシステムで、データストアの負荷を軽減して動的なウェブアプリケーションを高速化することを目的としている。データストアへのクエリの結果や、ウェブサイトの一部でレンダリングされたHTMLをキャッシュするのがMemcacheの典型的な使用例になる。

Memcacheパターン

Memcacheのパターンはきわめてシンプルだ:

  • ユーザまたはアプリケーションからクエリを抜き出す
  • クエリを満たすのに必要なデータがmemcache内にあるかどうか確認する
    • データがmemcacheにあれば、それを返す
    • データがmemcacheになければ、クエリをデータストアに投げてその結果をキャッシュに設定する

以下の擬似コードは典型的なmemcacheリクエストを理解してもらうためのものだ:

def get_data():
  data = memcache.get("key")
  if data is not None:
    return data
  else:
    data = self.query_for_data()
    memcache.add("key", data, 60)
    return data
guestbook.pyを改良してMemcacheを使うようにする

Getting Started Guideにあるゲストブックアプリケーションは毎リクエストデータストアにクエリを投げている。このゲストブックアプリケーションを改良して、データストアのクエリに頼る前にmemcacheを使うようにしてみよう。

まずはじめに、memcacheモジュールをインポートして、クエリ実行前にmemcacheを確認するメソッドを作成する。

from google.appengine.api import memcache

def get_greetings(self):
  """get_greetings()
  
  greetingsがキャッシュされているかどうか確認する。
  もしなければrender_greetingsを呼んで、キャッシュを設定する。

  戻り値:
    greetingsを含むHTML文字列
  """
  greetings = memcache.get("greetings")
  if greetings is not None:
    return greetings
  else:
    greetings = self.render_greetings()
    if not memcache.add("greetings", greetings, 10):
      logging.error("Memcache set failed.")
    return greetings

次にページのためのクエリの発行とHTMLの生成を分離する。キャッシュがヒットしないときは、このメソッドを呼んでデータストアにクエリを発行し、HTML文字列を作成した後、それをmemcacheに保存する。

def render_greetings(self):
  """render_greetings()
  
  データストアにgreetingsを問い合わせ、結果をイテレートして
  HTMLを作成する

  戻り値:
    greetingsを含むHTML文字列
  """
  results = db.GqlQuery("SELECT * "
                        "FROM Greeting "
                        "ORDER BY date DESC").fetch(10)
  output = StringIO.StringIO()
  for result in results:
    if result.author:
      output.write("<b>%s</b> wrote:" % result.author.nickname())
    else:
      output.write("An anonymous person wrote:")
    output.write("<blockquote>%s</blockquote>" %
                  cgi.escape(result.content))
  return output.getvalue()

最後にMainPageハンドラを変更してget_greetings()メソッドを呼ぶようにし、キャッシュが何度ヒットもしくはミスしたかという状況も表示する。

import cgi
import datetime
import wsgiref.handlers
import logging
import StringIO

from google.appengine.ext import db
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.api import memcache

logging.getLogger().setLevel(logging.DEBUG)


class Greeting(db.Model):
  author = db.UserProperty()
  content = db.StringProperty(multiline=True)
  date = db.DateTimeProperty(auto_now_add=True)


class MainPage(webapp.RequestHandler):
  def get(self):
    self.response.out.write("<html><body>")
    greetings = self.get_greetings() 
    stats = memcache.get_stats()
    
    self.response.out.write("<b>Cache Hits:%s</b><br>" % stats['hits'])
    self.response.out.write("<b>Cache Misses:%s</b><br><br>" %
                            stats['misses'])
    self.response.out.write(greetings)
    self.response.out.write("""
          <form action="/sign" method="post">
            <div><textarea name="content" rows="3" cols="60"></textarea></div>
            <div><input type="submit" value="Sign Guestbook"></div>
          </form>
        </body>
      </html>""")

  def get_greetings(self):
    """
        get_greetings()
        greetingsがキャッシュされているかどうか確認する。
        もしなければrender_greetingsを呼んで、キャッシュを設定する。

        戻り値:
           greetingsを含むHTML文字列
    """
    greetings = memcache.get("greetings")
    if greetings is not None:
      return greetings
    else:
      greetings = self.render_greetings()
      if not memcache.add("greetings", greetings, 10):
        logging.error("Memcache set failed.")
      return greetings

  def render_greetings(self):
    """
        render_greetings()
        データストアにgreetingsを問い合わせ、結果をイテレートして
        HTMLを作成する

        戻り値:
           greetingsを含むHTML文字列
    """
    results = db.GqlQuery("SELECT * "
                          "FROM Greeting "
                          "ORDER BY date DESC").fetch(10)
    output = StringIO.StringIO()
    for result in results:
      if result.author:
        output.write("<b>%s</b> wrote:" % result.author.nickname())
      else:
        output.write("An anonymous person wrote:")
      output.write("<blockquote>%s</blockquote>" %
                   cgi.escape(result.content))
    return output.getvalue()  
     
class Guestbook(webapp.RequestHandler):
  def post(self):
    greeting = Greeting()

    if users.get_current_user():
      greeting.author = users.get_current_user()

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


application = webapp.WSGIApplication([
  ('/', MainPage),
  ('/sign', Guestbook)
], debug=True)


def main():
  wsgiref.handlers.CGIHandler().run(application)


if __name__ == '__main__':
  main()

エンティティの関連をモデリングする

http://code.google.com/appengine/articles/modeling.html

はじめに

たしかにGetting Started Guideは単純なAppEngineモデルのプロパティを埋めるのに必要なことを教えてくれるけど、もしデータストアに現実世界の何かを表現できるようになりたいと言うならそれだけじゃ足りない。ウェブアプリケーション開発の初心者であろうと、SQLデータベースに馴染んでいようと、この記事はAppEngineのデータ表現の次の領域へ進みたい全ての人のために書かれている。

エンティティに関連が必要なのはなぜか

ユーザーが連絡先を保存できるアドレス帳を持つ、かっこよくて新しいウェブアプリケーションを構築中だと考えよう。ユーザが保存する連絡先のために、相手の名前、誕生日(絶対忘れちゃいけない)、住所、電話番号、会社を取得したい。

ユーザが住所を追加したいと思うと、フォームに情報を入力し、次のような感じのモデルにその情報が保存される:

class Contact(db.Model):

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # Phone info.
  phone_number = db.PhoneNumberProperty()
 
  # Company info.
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()

上出来だ。ユーザはすぐにこのアドレスブックを使い始め、データストアはすぐに埋まり始めるだろう。そしてこの新アプリケーションをデプロイしてしばらくすると、ユーザが電話番号をひとつしか持てないことを不満に思っていると知ることになる。ある人の家の番号だけじゃなく会社の番号も記録したいときはどうすればいいんだ?問題ないさ、と思い、君はデータ構造に会社の電話番号を付け加える。つまり次のようにだ:

# Phone info.
phone_number = db.PhoneNumberProperty()
work_phone_number = db.PhoneNumberProperty()

フォームに新しいフィールドを付け加えると君は仕事に戻る。アプリケーションを再びデプロイするとすぐにたくさんの不満を聞くことになるだろう。新しい電話番号フィールドが増えたことを知ると、さらにほかのフィールドも希望し始める。FAX番号がほしいという人もいれば、携帯番号がほしいという人もいる。携帯を複数持ってる人だっているだろう(いまどきの子供たちはホント大忙しだ)!FAXや携帯用のフィールドを一つ二つ追加することもできる。だけど携帯を3つ持ってたらどうする?10個だと?誰かが君が考えたこともないような電話を発明すると?

そのとき、モデルには関連が必要になる。

一対多

連絡先それぞれについて好きなだけたくさんの電話番号を設定できるようにするための答えはこうだ。そのためには電話番号それ自身をあらわすクラスと連絡先一つに複数の電話番号を関連付ける手段が必要になる。ReferencePropertyを使えば一対多関連のモデル化は簡単だ。新しいクラスは、たとえばこうなるだろう:

class Contact(db.Model):

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # 元のphone_numberプロパティは、暗黙的に作成される
  # 'phone_numbers'というプロパティと置き換えられる

  # Company info.
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()

class PhoneNumber(db.Model):
  contact = db.ReferenceProperty(Contact,
                                 collection_name='phone_numbers')
  phone_type = db.StringProperty(
    choices=('home', 'work', 'fax', 'mobile', 'other'))
  number = db.PhoneNumberProperty()

この方法のキーになるのはcontactプロパティだ。ReferencePropertyとして定義することで、Contact型の値だけを代入できるプロパティが定義できる。参照型のプロパティを定義すると参照されるクラスに暗黙的なコレクションプロパティが作成される。デフォルトではそのコレクションは_setという名前になる。今回だとContact.phonenumber_setというプロパティなるだろう。ただ、この属性はphone_numbersと呼んだほうが分かりやすいと思うはずだ。ReferencePropertyのキーワードパラメータとしてcollection_nameを渡してやればデフォルトの名前を上書きできる。

連絡先と、電話番号を一つ関連付けるのは簡単だ。"Scott"という連絡先が家と携帯の二つの電話番号を持つとしよう。その連絡先は次のように作成できる:

scott = Contact(name='Scott')
scott.put()
PhoneNumber(contact=scott,
            phone_type='home',
            number='(650) 555 - 2200').put()
PhoneNumber(contact=scott,
            phone_type='mobile',
            number='(650) 555 - 2201').put()

ReferencePropertyがContactに特殊なプロパティを追加してくれているので、ある人物が与えられたときに関係するすべての電話番号は簡単に手に入る。ある人物のすべての番号を表示したければ、次のようにすればいい。

print 'Content-Type: text/html'
print
for phone in scott.phone_numbers: 
  print '%s: %s' % (phone.phone_type, phone.number)

この結果は次のようになる:

home: (650) 555 - 2200
mobile: (650) 555 - 2201

注: デフォルトではこの種の関連では順序は保持されないので、出力の順序は異なるかもしれない

phone_numbers仮想属性はQueryのインスタンスなので、Contactに関するコレクションをさらに絞り込んだりソートしたりできる。たとえば、家の番号だけがほしければ次のようにする:

scott.phone_numbers.filter('phone_type =', 'home')

Scottが電話をなくしたときは、単にレコードを削除すればいい。PhoneNumberインスタンスを削除するだけでもうクエリには現れなくなる。

jack.phone_numbers.filter('phone_type =', 'home').get().delete()

多対多

ユーザが連絡先をグループにまとめて管理できるようにしたいとする。グループとしては「友人」「同僚」「家族」などがあるだろう。ユーザはそれらのグループをまとめて何かの処理、たとえば友人すべてにハッカソンの招待状を送ったりとか、ができる。まずは簡単に次のようなGroupモデルを定義しよう:

class Group(db.Model):

  name = db.StringProperty()
  description = db.TextProperty()

Contactには新しくgroupという名前のReferencePropertyを追加することもできるが、そうすると連絡先はたった一つのグループにしか所属できなくなる。たとえば同僚であり友人であるような人もいるだろう。多対多関連を表現する方法が必要だ。

キーのリスト

とても簡単なやり方は、関連の一方がキーのリストを持つようにすることだ。

class Contact(db.Model):
  # User that owns this entry.
  owner = db.UserProperty()

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # Company info.
  company_title = db.StringProperty()
  company_name = db.StringProperty()
  company_description = db.StringProperty()
  company_address = db.PostalAddressProperty()

  # Group affiliation
  groups = db.ListProperty(db.Key)

グループにユーザーを追加・削除することはキーのリストを操作することになる。

friends = Group.gql("WHERE name = 'friends'").get()
mary = Contacts.gql("WHERE name = 'Mary'").get()
if friends.key() not in mary.groups:
  mary.groups.append(friends.key())
  mary.put()

グループのメンバー全員が見たいときも、簡単なクエリを実行するだけでいい。Groupエンティティにヘルパ関数を作ると便利だろう。

class Group(db.Model):
  name = db.StringProperty()
  description = db.TextProperty()

  @property
  def members(self):
    return Contact.gql("WHERE groups = :1", self.key())

このやり方で多対多関連を実装した場合は制限が少しある。まず、利用できるのがKeyオブジェクトだけなので、リストが保持されているコレクションの値は明示的に検索してやらないといけない。さらに重要な制限は、ListPropertyにあまり大きなリストを保持するのは避けたほうがいいということだ。つまりリストに入れるのは関連のうちより少ない数になると思われる方にしたほうがいい。今回の例ではContactをリストに入れたが、これは一人の人が余りにたくさんのグループに所属することはなさそうだけど、グループは何百ものメンバを持ちそうだからだ。

関連モデル

ユーザの一人がトップクラスのセールスウーマンである会社のチームの人たちを知ってるとしよう。彼女は同じ会社の情報を何度も何度も入力しなければいけなくてとても面倒くさいと思う。特定の会社の情報は一度だけ入力してそれぞれの人をそこに関連付けるようにはできないんだろうか?もし単純な話なら単にContactとCompanyに一対多の関連を持たすだけでいいが、話はそう簡単でもない。彼女の連絡先の何人かは契約社員で複数の会社に所属しており、それぞれで異なる肩書きを持っている。さてどうする?

ここで必要なのは関連自身についてなにか追加の情報を持てるような多対多関連だ。そのためには関連を表すモデルを追加すればいい:

class Contact(db.Model):
  # User that owns this entry.
  owner = db.UserProperty()

  # Basic info.
  name = db.StringProperty()
  birth_day = db.DateProperty()

  # Address info.
  address = db.PostalAddressProperty()

  # The original organization properties have been replaced by
  # an implicitly created property called 'companies'. 

  # Group affiliation
  groups = db.ListProperty(db.Key)

class Company(db.Model):
  name = db.StringProperty()
  description = db.StringProperty()
  company_address = db.PostalAddressProperty()

class ContactCompany(db.Model):
  contact = db.ReferenceProperty(Contact,
                                 required=True,
                                 collection_name='companies')
  company = db.ReferenceProperty(Company,
                                 required=True,
                                 collection_name='contacts')
  title = db.StringProperty()

誰かに会社情報を追加したければContactCompanyインスタンスを作成すればいい。

mary = Contacts.gql("name = 'Mary'").get()
google = Company.gql("name = 'Google'").get()
ContactCompany(contact=mary,
               company=google,
               title='Engineer').put()

この方法を使うと、関連に関する情報を保持できるようになっただけではなく、キーのリストを持つ方法と比べて、よりたくさんの関連を処理できるという利点もある。ただし、関連をトラバースするのによりたくさんのデータストアへのアクセスが発生することになるので気をつける必要がある。この方式の多対多関連を使うのはそれが本当に必要な場合だけにして、実際に利用する場合もアプリケーションのパフォーマンスには気を配ろう。

結論

App Engineでは簡単にデータストアエンティティ間の関連を作成して、現実世界のアイデアを表現できる。ある一個のエンティティが不定数の情報を持つ必要があるならReferencePropertyを使おう。ほかのインスタンスとお互いに共有されるたくさんの異なるオブジェクトが必要ならキーのリストを使おう。この二つのアプローチでアプリケーションで使用するモデルのほとんどをカバーできることに気づくだろう。