先日、「Surgo: BigTableで悩む」という記事を見つけた。Google App EngineでTwitterのようなアプリケーションを作るにはどうすればいいかについて悩んでいるらしい。どうやら「Surgo: BigTableはPUSH型で」によるとPUSH型で作るということで解決(?)はしたようだが、PULL型でgoogle.appengine.ext.db.Queryの機能を使ってソートしたデータを取得するようなことはできないのだろうか?

Google App Engineの練習問題としては面白そうだったので、実際に簡単なサンプルを作って考えてみることにした。

(※実は私はTwitterを使ったことがなく、あまり詳しくは知らないので機能に関して誤解があるかもしれない。)


とりあえず、問題は「自分がFollowしたUserの全てのItem+自分が受信したItem」をどうやってソートして取得するかだ。DBから取得したあとに自分でソートする方法もあるが、今回はDBからソートしたデータを取得する方法を考える。

Google App EngineではほとんどOR検索ができないが、不等号(<,>,!=)による比較とINによる比較を用いることである程度はOR検索をすることができる。ただし、google.appengine.ext.db.Queryは!=やINによるfilterをサポートしていないようなので、!=やINを使う場合にはgoogle.appengine.ext.db.GqlQueryを使う必要がある。

(「Queries and Indexes - Google App Engine - Google Code」と「The GqlQuery Class - Google App Engine - Google Code」参照。)

今回の場合、INを使うことで実装ができそうだ。

まずデータ構造は以下のようにする。

class Account(db.Model):
    user = db.UserProperty(required=True)
    follow = db.ListProperty(item_type=db.Key)
  
class Item(db.Model):
    # 発言者
    account = db.ReferenceProperty(required=True, reference_class=Account, collection_name=u'Account1')
    # 全体に公開する発言のときsend_from=発言者、特定のユーザに対する発言のときsend_from=None
    send_from = db.ReferenceProperty(reference_class=Account, collection_name=u'Account2')
    # 特定のユーザに対する発言のときsend_to=送信相手、全体に公開する発言のときsend_to=None
    send_to = db.ReferenceProperty(reference_class=Account, collection_name=u'Account3')
    # 発言内容
    text = db.StringProperty(required=True)
    # 時間
    published = db.DateTimeProperty(required=True, auto_now_add=True)

この形式で保存されたデータに対して以下のようにGQLを作ってデータを取得する。

# 自分がFollowしたAccountの全てのItem + 自分が受信したItem を取得
read_items = None
if account.follow:
    read_items = Item.gql(
        u'WHERE send_to IN (:1, :2) AND send_from IN (%s) ORDER BY published DESC'
            % (u', '.join([u':%d ' % (i+3) for i in xrange(len(account.follow)+1)])),
        None, account,
        *tuple([None]+[Account.get(key) for key in account.follow])
    )
else:
    read_items = Item.gql(
        u'WHERE send_to = :1 ORDER BY published DESC',
        account,
    )

もしaccount.followがサイズ3のリストであれば、以下のようなGQLが作成される。

WHERE send_to IN (:1, :2) AND send_from IN (:3, :4, :5, :6) ORDER BY published DESC

:1~6のうち:1と:3はNoneとなり、:2は自分のアカウントである。:4~6はFollowしているユーザとなる。

send_toとsend_fromはItem作成時に以下のようにどちらかが必ずNoneになるようにする。

全体に公開するメッセージ特定のユーザに送るメッセージ
send_from送信者None
send_toNone受信者

これにより「自分がFollowしたUserの全てのItem+自分が受信したItem」をWHEREで絞りこむことが可能になり、ORDERを使ってソートすることができるようになる。

サンプルソース(main.py)

ただし、この方法ではINに指定できる項目数の制限にひっかかってしまうようで、account.followがサイズ15以上になると以下のようなエラーが表示されるようになる。

BadArgumentError: Cannot satisfy query -- too many IN/!= values.

これではTwitterのようなアプリケーションは作ることはできそうにない。Followの数を制限すればなんとかなりそうだが、15人程度しか登録できないのでは数が少なすぎる。

そうなると「Surgo: BigTableはPUSH型で」のような解決方法を取るか、取得したデータを自分でソートするような方法を取るしかないのだろうか?Google App Engineでは、書き込みが非常に遅いため大量の書き込みをするような操作ができなかったりすることもあるし、リレーショナルデータベースと違いすぎてどう設計すればいいのか未だによくわからない。Google App Engineではこういった問題をどう解決するのが一般的なのだろう?