rediscoを使ってみた

rediscoって何?

redisco とは RedisDjango モデルっぽく使えるようにしてくれるライブラリ。
redis-py に依存している。
今回MySQLのTPSだと問題になりそうな処理がある & その処理にある程度複雑なモデルが必要だったので @ に教えてもらって使ってみた。

使い方

  • 接続
import redisco
redisco.connection_setup(host='localhost', port=6380, db=10)

保存や取得をする前には接続する必要がある。connection_setup を呼ぶことで redis との接続を作って保持してくれる。

  • モデル定義
from redisco import models
class AnimalType(models.Model):
    name = models.Attribute(required=True)

class Animal(models.Model):
    name = models.Attribute(required=True, indexed=False)
    animal_type = models. ReferenceField(AnimalType, required=True)
    created_at = models.DateTimeField(auto_now_add=True)
    coord_x = models.IntegerField(equired=True, indexed=False)
    coord_y = models.IntegerField(equired=True, indexed=False)

こんな感じでモデルを作れる。
Django と比べると
CharField -> Attribute
ForienKey -> ReferenceField
になってるのが主な違い。

  • 保存
cat = Animal.objects.create(name=u'ネコ')
animal = Animal()
animal.name = u'タマ'
animal.type = cat
animal.coord_x = 1
animal.coord_y = 1
animal.save()

オブジェクトを作って保存する方法は Djangoモデルと同じ。
保存時に Django と同じように id を作ってくれる。
ただし id が int じゃなく数値文字列('10'とか)なので注意。

  • 取得

普通に取って来るだけなら(遅延評価してるとこも含めて) Django と変わらない。(get はないっぽいけど)

cats_query = AnimalType.objects.filter(name=u'ネコ')

cats = cats_query.members
# これも同じ
cats = list(cats_query)

cats_count = len(cats_query)

cat = cats_query.first()
# これも同じ
cat = cats_query[0]


ただ、indexd=False なフィールドを条件に指定することは出来ない。デフォルトが index=True なので、絶対に条件に指定しないフィールド以外は index=False にしないこと推奨。

# これはOKだけど
animal = Animal.objects.filter(animal_type=cat)[0]
# これはエラー
tama = Animal.objects.filter(name=u'タマ')[0]


また、filter() では id を条件に指定することは出来ないので、id で引きたい場合は、get_by_id() を使う。

# これはエラーなので
animal = Animal.objects.filter(id=1)[0]
# こっちを使う
# tama.id は str だけど、ここに指定するのは int でOK
tama = Animal.objects.get_by_id(1)


Djangoみたいに __in とか __lte とかも使える。その場合は filter() の代わりに zfilter() を使う。ただ、zfilter() だと list() で評価は出来ないし、複数の条件も指定出来ない。

# これは出来るけど
animals_query = Animal.objects.zfilter(created_at__lte=datetime.datetime.now())
# これは出来ない
cats_query = Animal.objects.zfilter(created_at__lte=datetime.datetime.now(), animal_type=cat)

# これは出来るけど
animals = animals_query.member
animals.count = len(animals_query.member)
animal = animals_query.member[0]
# これは出来ない
animals = list(animals_query)
animals = len(animals_query)
animal = animals_query[0]


使ってみた感想
redisco を使うと、保存時に自動的にトランザクションを使ってくれるし key を自分で考えたりせずに使えるので非常に楽だった。
ただしDjangoモデルとは微妙に違うし、ドキュメント少ない上に日本語のものは皆無なのでソースを追ったりする必要があったのが面倒。
今回は使ってないけど、Djangoモデルっぽいもの以外に Set とかも作れるので単純な Key-Value の用途以外で使う場合は検討してみる価値が十分にあると思う。

DjangoのurlresolversでURL以外を振り分ける

リクエストを即時実行したくない

現在作ってるゲームでは諸々の都合で複数のリクエストを一度キューに貯めて後からまとめて実行する仕組みが必要になっている。
そういう時にどうするかってのを、@cactusman と検討したのだけど MQ や Celery をそのままでは上手くいかなそうだったので、結局 Django モデルでキューっぽいテーブルを実装した。

class QueueTask(models.Model):
    """                                                                                                                                                                                                   
    キューに詰むタスク                                                                                                                                                                        
    """
    request = models.CharField(verbose_name=u'操作のリクエスト', max_length=256)
    created_at = models.DateTimeField(verbose_name=u'キューへの追加日時', auto_now_add=True)
    status = models.IntegerField(verbose_name=u'実行ステータス', default=0)

みたいな感じで。

で、この操作のリクエストによって実行内容が異なるのだが、その辺の振り分けを自分で書くのが面倒だったので urlresolvers を流用することにした。


urlresolvers の流用
前述の QueueTask のメソッドとしてタスクの実行部分はこんな感じになっている。

    def exec_task(self):
        resolver = urlresolvers.RegexURLResolver(r'^/', 'queue_manager.task')
        callback, callback_args, callback_kwargs = resolver.resolve(self.request)
        result = callback(*callback_args, **callback_kwargs)
        self.save()

あとは、queue_manager/task.py を urls.py と同じように

urlpatterns = patterns(
    'animal.task',
    url(r'^task/animal/put/(?P<animal_id>\d+)/$', 'put', name='put_animal'),
)

みたいにして、view関数 みたいにタスクの実行部分を書けばOK。
注意点としては、urlpatterns って名前は内部で決め打たれてるので変えちゃダメ。

DjangoのForeinKeyとかキャッシュとか改

毎回キャッシュアクセスはしたくない

前回、DjangoのForeinKeyとかキャッシュとか - ま、そんな日もあるさを書いたんですけど

name = player.cached_community.name
id = player.cached_community.id

みたいに使うと複数回キャッシュを見に行ってしまうので

community = player.cached_community
name = community.name
id = community.id

みたいに変数にコピーして使っていました。
でも、毎回これは面倒!


改良してみた

class Community(AbustractCachedModel):
    name = models.CharField(max_length=50)

class Player(AbustractCachedModel):
    name = models.CharField(max_length=50)
    community = models.ForeinKey(Community)
    community_cache = None
    @property
    def cached_community(self):
        if self.community_cache is None:
            self.community_cache = Community.get(self.community_id)
        return self.community_cache

これで、何も考えずに

name = player.cached_community.name
id = player.cached_community.id

みたいに使えばOK。


気をつけること
communityを更新しても自動ではcached_communityは更新されないので、そこんとこだけ注意。

DjangoのForeinKeyとかキャッシュとか

キャッシュ機能付きモデルの欠点

以前、Djangoでキャッシュ機能付きモデル - ま、そんな日もあるさを書いたんですけど、これって ForeinKey を持つようなモデルだと思ったように上手く動いてくれません。

class Community(AbustractCachedModel):
    name = models.CharField(max_length=50)

class Player(AbustractCachedModel):
    name = models.CharField(max_length=50)
    community = models.ForeinKey(Community)

みたいな、モデルがあった時に

player = Player.get(1)
player.community

みたいにアクセスしてしまうとキャッシュから読むのではなく SQL が発行されてしまいます。


解決策

そういう場合は

class Player(AbustractCachedModel):
    name = models.CharField(max_length=50)
    community = models.ForeinKey(Community)

    @property
    def cached_community(self):
        return Community.get(self.community_id) 

みたいにプロパティを定義して

player = Player.get(1)
player.cached_community

みたいなアクセスをすれば OK です。
ここで使っている community_id は Django で ForeinKey なフィールドを作った時に実際のテーブルに追加されるフィールド名です。(フィールド名が xxx だったら xxx_id になります)


もうちょい詳しい話

player.community

みたいなアクセスの場合(実際には Django 内部のキャッシュとか色々ありますが)簡略化すると以下のような処理が走るので SQL が発行されてしまいます。

Player.objects.get(id=player.community_id)

なので、player を取得した時点でわかっている community_id を使ってキャッシュからアクセスすることで、SQL を発行せずに community を取得することが可能になります。
なお、community の代わりに community_id を使って SQL の発行回数を減らすノウハウは結構使うので覚えておくと良いかもしれません。

GWとかを振り返って

今年のGWは何もしなかった
月曜を休んで7連休だったのだが、30日に服を買いに行ったのと2日に奥様の付き添いで病院に行った以外は近所のスーパーくらいしか行かずに引きこもっていた。
それだけ時間があるなら勉強でもすれば良いのだが、それすらせずに毎日体調の悪い奥様に付き合って寝てたりアニメ見たりしてた。
せめてブログくらい書こうと思ったが、アウトプット出来るようなことは何もない。
なのでBPに転職して2ヶ月でやってたこととかを書いてみようと思う。


ゲームを作るという仕事
BPで何をやっているかというとPC向けのソーシャルゲームブラウザ三国志とかサンシャイン牧場みたいな)に携わっている。
僕は、企画があってプロデューサ的な人(以下Nさん)がいて、大まかなゲームの形が出来てるところから参加した。
まだ実際のゲームに使われるコードは1ミリも書いていない。(一週間くらいだけどプロトタイプ用のコードは書いた。)
ここのところやっているのは、Nさんが作った仕様を見て、「課金が…」とか、「システム的には…」とか、「これ必要ですか?」とか、「この意図なら別の方法のが良くないですか?」とか、ダメ出しをするだけの簡単なお仕事をしている。
役割的にはNさんが仕様を決めるのを手伝ってる感じだ。
何の因果でこんな偉そうなことをしてるのかはわからないが、g社とかMtGの経験とかが関係してるらしい。
こんなことを2月近くやってきてわかったのは、僕は別にゲームを作りたい訳じゃないってことだ。
仕様もほぼ決まり、ようやく本格的にコードが書けそうで今は凄くほっとしている。


プロトタイプのこととかSkypeBotのこととか
技術っぽいことで書こうと思ったこともあるのだけど、眠いのでまた次回。

randomがどれくらい収束するのか試してみた

twitterで50枚のものから1枚を選ぶのを延々と繰り返した場合に、最も選ばれるものと最も選ばれない物とでどれくらい差が出るのかって話をしてたので試してみた。
ソースはこんなの。

import random

count_list = [0] * 50

for i in xrange(10000 * 100):
    count_list[random.randint(0,49)] += 1

結果は

>>> count_list.sort()
>>> count_list[0]
19642
>>> count_list[-1]
20243

試してみる前は最大と最小で2倍程度の差があるのかと思っていたのだが、実際は5%くらいしか差がなかった。

ちなみに最初何も考えずにrange()で1億回くらい実行してメモリが足りなくなったのは秘密。

Google MapsのKMLを触ってみた

Google MapsのマイマップからRSSが無くなっていたので、社内用SkypeBotをRSSからKMLへ変更してみた。
その時いくつかハマったのでメモ代わりに書き残す。
KMLのパースにはElementTreeを使った。
コードはこんな感じ。

import urllib2
from xml.etree import ElementTree

KML_FEED = 'http://maps.google.com/maps/ms?msid=[MSID]&output=kml'
NAMESPACE = 'http://earth.google.com/kml/2.2'

xml = ElementTree.parse(urllib2.urlopen(KML_URL))
entries = xml.findall('.//{%s}Placemark' % NAMESPACE)


ハマった点

  1. KMLにはIDがないので吹き出しを表示するURLを生成出来なくなってしまった。
  2. Google MapsのURLにあるllパラメータは北緯、東経の順なのに、KMLのcoordinatesタグは東経、北緯の順だった。
  3. ネームスペースを指定しないとタグを取って来れなかった。

KMLファイルをエディタで見てもネームスペースは書いてないので

xml.getroot().tag

で根エレメントのネームスペースを確認して、それを指定する必要があった。


結局1は解決出来ていないので、吹き出しを表示するURLの作り方を知ってる人は教えてください。