クラスベースビューとreverseとDjangoのURLディスパッチ

以下、django1.2.0時点の話です。


参照: クラスベースビューとデコレータとkwargs - logiqboard

viewクラスを使ったURL設計

前回の記事でいうところのshopとgoodsのように、URL・機能的にはshopという大枠にまとまってはいるんだけど、その中に小機能がいっぱいあってshopにベタ実装するときたねーという場合は、上位アプリに完全依存するサブアプリみたいのを作るのも一つの手です。


再掲ですがurlsのところはこんなイメージ。

class ShopViews(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("$", self.index, name=u"index"),
            url("goodsA/", include(goodsAView("goodsA").urls),
            url("goodsB/", include(goodsBView("goodsB").urls),
        )

class goodsA(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("^$", self.index, name=u"index"),
            url("^(?P<goodsA_id>\d+)/", self.detail, name=u"detail"),
            url("^(?P<goodsA_id>\d+)/buy", self.buy, name=u"buy"),
            以下多数
        )

rootのurlconfには下記の形式でinclude

...
url("^shop/(?P<shop_id>\d+)/", include(ShopView("shop").urls)),
...

実はこの設計はうまく動きません。

正確にはURLの正引きは問題ないんですが逆引きがうまくいきません。

どういうことなの

上記の設計だと

>>>reverse("shop:goodsA:detail", args=[shop_id, goods_id])
"/shop/1/goodsA/123/"

となってくれそうなものですが、実際はNoReverseMatchとなります。まったくひどい罠です。

原因

詳しい原因は長いので下の方に書くとして、とりあえず原因。

url設計をする際に

url("mypage/(?P<id>\d+)/", include(MyViews().urls)),

のように、「引数をキャプチャするようなURLパターンに対してincludeを行なう」と発生します。

回避する

回避方法としては、悲しいことですが原因のところに書いてあるような設計をしないようにするしかありません。

はうはう

詳しい理由

url関数はRegexPatternを生成するものですが、viewを渡す部分でincludeをしている場合代わりにRegexURLResolverが生成され、その内部にinclude先のRegexURLPatternのリストを保持します。

RegexURLResolverは更にそれ自身の名前空間とそれが割り当てられるURLパターン文字列を保持します。

reverseの際は

1. 渡されたnameを:で分割してviewとnamespaceのリストに分け

2. namespaceのリストを先頭から順番に使ってURLResolverを全て辿り

3. 辿り着いた先でview名からURLPatternを探し

4. 見つかったURLPatternとreverseに渡されたargs, kwargsをマッチングし

5. マッチしたらそのURLパターンにargsを適用し、URL文字列にして返す

という処理がされているのですが、2番でURLResolverが持つURLパターンを結果URLとして連結する際に引数の解決がされていないようです。

実例

shopとgoodsの例では、shop_idを取るURLパターンがURLResolverに割り当てられているため、ここが解決されずに正規表現文字列のまま連結されてしまいます。

また、goodsAのname=detailのURLPatternではgoodsA_idしか取っていないにもかかわらず、argsとしてはshop_idとgoods_idが渡されるためマッチしないと判断され、NoReverseMatchとなります。

おしまい

僕はURL設計がばっちくなるのが嫌だったのでinclude_to_rootなんていうメソッドを作って、includeするURLパターンがが全て連結されてurlconfに直接登録されるようにして回避してみました。

うれしくなーい

クラスベースビューとデコレータとkwargs

クラスベースビューとデコレータを駆使するとDRYに書けていいですねーという話。

たとえば

こんなURL設計のページを実装するとする。

/shop/<shop_id>/
/shop/<shop_id>/goodsA/<goodsA_id>/
/shop/<shop_id>/goodsA/
/shop/<shop_id>/goodsA/<goodsA_id>/buy
/shop/<shop_id>/goodsA/<goodsA_id>/confirm
/shop/<shop_id>/goodsA/<goodsA_id>/buy_finish
/shop/<shop_id>/goodsB/
/shop/<shop_id>/goodsB/<goodsB_id>/
/shop/<shop_id>/goodsB/<goodsB_id>/buy
/shop/<shop_id>/goodsB/<goodsB_id>/confirm
/shop/<shop_id>/goodsB/<goodsB_id>/buy_finish
.
.
.

べた書きするとひじょうにめんどそうな感じですが、クラスベースビューを使ってshop, goodsA, goodsBの3つのアプリに分け・・・

class ShopViews(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("^shop/(?P<shop_id>\d+)/$", self.index, name=u"index"),
            url("^shop/(?P<shop_id>\d+)/goodsA/", include(goodsAView().urls),
            url("^shop/(?P<shop_id>\d+)/goodsB", include(goodsBView().urls),
        )

などして、適宜goodsAView, goodsBViewでbuy, confirmなどのビューを実装してやればビューの分割が綺麗にできてうれしさ満点です。*1

しかし

この場合goodsAのビューメソッドがどうなるかというと

class goodsA(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("^$", self.index, name=u"index"),
            url("^(?P<goodsA_id>\d+)/", self.detail, name=u"detail"),
            url("^(?P<goodsA_id>\d+)/buy", self.buy, name=u"buy"),
            以下多数
        )
     
    def index(self, shop_id):
        return get_object_or_404(Shop, pk=shop_id)

    def detail(self, shop_id, goodsA_id):
        shop = get_objects_or_404(Shop, pk=shop_id)
        goods = get_objects_or_404(GoodsA, pk=goodsA_id)
        return (shop, goods)

    以下延々と

こんな感じですね。

なにがめんどいかと言えば

  1. shop_idからShopオブジェクトを取得するところが分離できてないので同じ処理をたくさん書かなきゃいけない
  2. goods_idも同様
  3. ビューメソッドにshop_idとかいっぱい出てくるのでダサい
  4. でもURLパターンでshop_id取っちゃってるからビューメソッドの引数からなくすとエラー


やだー

どうする

このままではダサいうえに変更に弱くなるのでなんとかしましょう。

まずこんなデコレータを書きます。

(shop/views.pyとか)

def shop_view(view_func):
    def _view(request, *args, **kwargs):
        _kwargs = copy(kwargs)
        shop_id = _kwargs.pop("shop_id")
        shop = get_object_or_404(Shop, shop_id)
        request._shop = shop
        return view_func(request, *args, **kwargs)
    return _view

(goodsA/views.pyとか)

def goods_view(view_func)
    def _view(request, *args, **kwargs):
        _kwargs = copy(kwargs)
        goods_id = _kwargs.pop("goodsA_id")
        goods = get_object_or_404(GoodsA, goods_id)
        request._goods = goods
        return view_func(request, *args, **kwargs)
    return _view

あとはgoodsAのビュークラスに仕込むだけ。

class goodsAView(object):
    
    ...

    @shop_view
    def index(self, request):
       return request._shop

    @goods_view
    @shop_view
    def buy(self, request):
       return (request._shop, request._goods)

ちょうすっきりしました。デコレータはよいものですね。

要点1

shop = get_object_or_404(Shop, shop_id)
request._shop = shop

Shopオブジェクトを取得し、なければ404を返すというのをデコレータ上で処理させます。

ビュー周りではデコレータをかけるだけなので見通しがよく、つまらんコードをコピペする必要もなくなります。

要点2

goods_id = _kwargs.pop("goodsA_id")

kwargsからpopしています。

urlから取られた引数のdict(kwargs)がそのままviewに渡されるので受け取る側と引数がマッチしていないとエラーが出ますが*2、こいつをビューに渡る前にとっぱらってやるわけです。

要点3

_kwargs = copy(kwargs)

kwargsままでなく、コピーしています。

元のkwargsを弄ってしまうと、shop_viewより外側で適用されてかつview実行の後処理を行なうデコレータがあった場合にkwargs変更の影響が及んでしまい、危険です。




おしまい。

*1:例なので説明しやすさ重視で変なURLの切り方ですが、ほんとならshop/のとこは/urls.pyに書いて、そこにShopViews().urlsをincludeするのが綺麗だと思います。

*2:勿論ビュー側仮引数にデフォルト値を指定すれば回避できますが、shop_idの例でデフォルト値指定するのはバグの元になりそうだし、結局shop_idという仮引数は書かなくちゃならないので意味ナシ。

Django+Tritonnで日本語全文検索の追記

Django+Tritonnで日本語全文検索〜重み付けもあるよ〜 - logiqboard

テーブルの持ち方

レプリケーションのスレーブの一つをサーチ用にして、そこのBookテーブルにインデックスを張ればよいという話をしてもらった。

なるほどそれならマスタ書き込み系に影響を与えることもない。

そもそもよっぽど妙な検索をしようとしない限りはわざわざコピーをBookSearchにもたずとも既存のBookのカラムにFulltext indexを張ってしまえばいいわけで、かなり合理的ではなかろうか。

ただし

当然全文検索クエリは常にサーチ用スレーブに飛ぶように設定しなくてはいけない。

java-ja温泉2まとめ

ダブルスポイラー
4までやった
記事
3件消化。
botengineがやっとできた
汎用Bot記述ライブラリみたいな形に落ち着いた。スペシャルサンクス id:nullpobug
育英した
した。

やりのこしたこと0。いい温泉でござった。

また行きます。

EC2とMySQLとダンプリストア

EC2上のMySQLで、大量データをダンプリストアしようとしたらとんでもない時間がかかりましたという話。

経緯

EC2で運用してるサイトのMySQLのibdata1が結構なサイズに膨らんでしまったので、ダンプリストアして縮小+ibdata1を分割しようとした。

誤算

7000万件、50Gぐらいのダンプを出すのに1hかかった。これは想定内。

ただ、インサートに8時間かかった。勿論--opt付きで。

勿論メンテ時間オーバーで大変悲しい目にあった。

なんでやねん

EC2(EBS)への書き込みって平均25MB/secぐらいしかでてないっぽい。

加えて単なる書き込みじゃなくてインサートだったので更に遅くなったっぽい。

結局

インサートでの復旧は断念して、バックアップからファイルコピーで復旧。

こっちは2時間ぐらいで終わった。

結論

EC2でそういうことするのはやめましょう。

というかクラウド全般でI/O速度がネックになるような作業をするなという話ですね。わかります・・・

今回みたいなことをしたい場合は

時間を気にしなくてもいいようにする
うまくやろうぜ
普通の環境でデータを作ってからファイルコピーで復旧する
そもそもEC2でやらなくてもいいじゃん・・・


という感じで。

いい勉強になりました。

Django+Tritonnで日本語全文検索〜重み付けもあるよ〜

DBにTritonnを使ってDjangoから全文検索をする という話

Tritonnってなに?どうやって入れんの

かつあい

モデル構造

class Book(models.Model):
    """本"""
    title = models.CharField(max_length=255)
    desc = models.TextField()
    pub_date = models.DateTimeField(auto_now_add=True)
    
class BookSearch(models.Model):
    """本の検索テーブル"""
    book = models.OneToOneField(Book)
    title = models.CharField(max_length=255)
    desc = models.TextField()

全文検索に使いたいカラムを持った〜〜Searchモデルみたいなのを定義。

OneToOneとして元オブジェクトにリレーションを張る。

下準備

当然このままだとsyncdbしても(多くは)InnoDBだわFulltext index張られてないわなので、BookSearchモデルを定義したアプリのフォルダにsql/BookSearch.sqlを書いてテーブル定義をむりくり書き換える。

DROP TABLE `book_search`;
CREATE TABLE `book_search` (
  にゃんにゃん,
  FULLTEXT KEY `search` USING MECAB, NORMALIZE, SECTIONALIZE, 256 (`title`, `desc`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

manage.py sqlで出したテーブル定義をまるコピしつつ既存テーブルをDROPしてfulltext keyとEngine=MyISAMを付けて再定義する感じ。

シグナル定義

BookのsaveにフックしてBookSearchも作らないと検索出来ないですよね。
なのでシグナル書く。

def book_search_signal(sender, instance, **kwargs):
    try:
        bs = BookSearch.objects.get(book=instance)
    except:
        bs = BookSearch()

    bs.title = instance.title
    bs.desc = instance.desc
    
    bs.save()

models.signals.post_save.connect(book_search_signal, sender=Book)

検索メソッド

パフォーマンスを気にするのであればBookSearchからQuerySetを作るべきだけど、そうすると最終的に出てくるのがBookSearchオブジェクトのリストになってしまうのでとりまわしが面倒。
取り回しを重視するのであればBookから作ってBookオブジェクトのリストを得ると楽でよいです。

今回は後者で。

class BookManager(models.Manager):

    にゃんにゃん

    def fulltext(self, keywords):
        match = u"""MATCH (
                        `book_search`.`title`,
                        `book_search`.`desc`,
                        ) AGAINST (
                        %s IN BOOLEAN MODE
                    )"""

        param = "*W1:3,2:1 %s" %  u" ".join([u"+" + x for x in keywords])

        query = self.be()\
                    .extra(
                    select=SortedDict([
                        ("fulltext_score", match),
                    ]),
                    select_params=(
                        param,
                    ),
                    tables=[
                        "`book_search`"
                    ],
                    where=[
                        match,
                        """
                        `book_search`.`book_id` = `book`.`id`
                        """
                    ],
                    params=[
                        param,
                    ],
                ).order_by("-fulltext_score")
        return query

extraでがんばって最終的に重み付け検索+fulltext_scoreでソートしたquerysetを得る。

SELECTカラムにfulltext_scoreなるカラムをつける辺りが結構複雑なので、MySQL側のクエリログを見ながら試行錯誤する必要あり。*1

使う

こんな感じに使える。

Book.objects.fulltext(["ぬる", "ぽ"]).filter(pub_date__gte=datetime.now()-timedelta(days=7))

過去一週間以内に発売された、"ぬる"か"ぽ"をタイトルか紹介文に含むBookオブジェクト。

おわり

わりと富豪的プログラミングだと思うので、お使いの際はデータ量等々とご相談の上どうぞ。

*1:queryset.query.as_sql()は信用ならないです。