日記

日々のことと、Python/Django/PHP/Laravel/nodejs などソフトウェア開発のことを書き綴ります

Middleware その2

前回から間が空いてしまいましたが、Middlewareに関連したことを一歩進んで勉強してみます。

Middlewareの弱点

Middlewareは、Djangoフレームワーク上の拡張ポイントです。JavaServlet APIのFileterに似てます。ここまでは前回説明しましたが一つ問題が。

「エンドポイント関数単位の使用の有無が選択できない」

Middlewareを利用する時には、Djangoフレームワーク上では ON か OFF か、しか指定がありません。もし、特定の関数や特定のURLにだけ共通的な機能を提供したい、と言った時に非常に不便です。しかし、そんな機能は Djangoでは標準サポートに含まれていないようです。

そこで、Djangoで標準サポートされているトランザクション管理がどんなことをやっているか見てみたいと思います。だって、トランザクションだったら、処理成功で commit 失敗したら rollback 出来ないとだめだろうし、読み込むだけだったら readonlyにしたいですよね。さらに言えば、トランザクションを使わないことだってある!絶対指定できるはずだ!!

TransactionMiddlewareの中身

早速、設定ファイルに書かれている「django.middleware.transaction.TransactionMiddleware」を見てみます。

from django.db import transaction

class TransactionMiddleware(object):
    """
    Transaction middleware. If this is enabled, each view function will be run
    with commit_on_response activated - that way a save() doesn't do a direct
    commit, the commit is done when a successful response is created. If an
    exception happens, the database is rolled back.
    """
    def process_request(self, request):
        """Enters transaction management"""
        transaction.enter_transaction_management()
        transaction.managed(True)

    def process_exception(self, request, exception):
        """Rolls back the database and leaves transaction management"""
        if transaction.is_dirty():
            transaction.rollback()
        transaction.leave_transaction_management()

    def process_response(self, request, response):
        """Commits and leaves transaction management."""
        if transaction.is_managed():
            if transaction.is_dirty():
                transaction.commit()
            transaction.leave_transaction_management()
        return response

意外と簡単ですね。ほとんどの処理は、「django.db.transaction」に委譲されてしまっています。ここまで単純だと、切り替え的な処理は全くやってい無さそうです。
読み取れるのは、

上記のことです。ということで、何かしら関数ごとの処理分けがされていることを期待してたのに、Djangoトランザクション管理では、「処理分けは成功失敗のみ」で、「トランザクション機能の有効無効はMiddlewareのON/OFFに完全依存」していることが分かりました。

ちなみに

        transaction.enter_transaction_management()
        transaction.managed(True)

で何をやっているかと言うと、最初に呼び出す関数で、リクエスト(Thread)が使用するコネクションをThreadローカル変数に保存して、次の関数でコネクションを初期化しています。

コネクションの初期化タイミングに違和感がある実装で、本当にDjangoは大丈夫なんだろうかと不安になったのは内緒です。

TransactionMiddlewareには期待できない

使えないっていう話ではなくて、期待してた実装は行っていないようです。
途方に暮れてしまうところでしたが、よさそうなのを見つけました。

Controlling transaction management in views

http://docs.djangoproject.com/en/1.1/topics/db/transactions/#controlling-transaction-management-in-views

これです。
要は、Javaアノテーションと同じような記述で処理を関数呼び出しの前後に処理を挟める機能です。ちなみにこの機能は Django ではなくて Python の機能です。(ややこしい...)そんな事情もあって、Djangoでは関数単位の制御の仕組みをフレームワーク側ではもたないのでしょう。

transaction デコレータ

@transaction.success_on_commitと書けば成功したときだけコミット、@transaction.autocommitで自動的にコミット(トランザクション無し)、@transaction.commit_manuallyで手動トランザクション制御となります。

この機能は、Middlewareではないので「django.db.transaction」に定義されています。実装を見てみると

def autocommit(func):
    """
    Decorator that activates commit on save. This is Django's default behavior;
    this decorator is useful if you globally activated transaction management in
    your settings file and want the default behavior in some view functions.
    """
    def _autocommit(*args, **kw):
        try:
            enter_transaction_management(managed=False)
            managed(False)
            return func(*args, **kw)
        finally:
            leave_transaction_management()
    return wraps(func)(_autocommit)

def commit_on_success(func):
    """
    This decorator activates commit on response. This way, if the view function
    runs successfully, a commit is made; if the viewfunc produces an exception,
    a rollback is made. This is one of the most common ways to do transaction
    control in web apps.
    """
    def _commit_on_success(*args, **kw):
        try:
            enter_transaction_management()
            managed(True)
            try:
                res = func(*args, **kw)
            except:
                # All exceptions must be handled here (even string ones).
                if is_dirty():
                    rollback()
                raise
            else:
                if is_dirty():
                    commit()
            return res
        finally:
            leave_transaction_management()
    return wraps(func)(_commit_on_success)

def commit_manually(func):
    """
    Decorator that activates manual transaction control. It just disables
    automatic transaction control and doesn't do any commit/rollback of its
    own -- it's up to the user to call the commit and rollback functions
    themselves.
    """
    def _commit_manually(*args, **kw):
        try:
            enter_transaction_management()
            managed(True)
            return func(*args, **kw)
        finally:
            leave_transaction_management()

    return wraps(func)(_commit_manually)

こんな感じです。それぞれの関数の中で内部関数を定義し、内部関数にView関数を渡しています。

デコレータの作り方

fooデコレータを定義し views.py に組み込むと

# vim:fileencoding=utf-8
from django.shortcuts import render_to_response

from django.http import HttpResponse
from django.template import loader
from django.template import RequestContext

from functools import wraps

def foo(func):
  def wapper_function(*args, **kw):
    print 'before'
    res = func(*args, **kw)
    print 'after'
    return res
  return wraps(func)(wapper_function)

@foo
def first(request):
    print 'call first.'
    from datetime import datetime
    
    t = loader.get_template('first.html')
    c = RequestContext(request, {
      'abc': u'abcですと!',
      'none_data': None,
      'empty_string': '',
      'two_value': 2,
      })
    return HttpResponse(t.render(c))

first関数の上に@で始まる行を追加しています。これがデコレータ指定の方法です。
で、このコードをブラウザ経由で実行すると

call process_request.
call process_view.
before
call first.
after
call process_response.

という感じで、ちゃんと before/after もテキストが出力されています。

まとめ

  • アプリ全体の共通処理は Middleware に実装する。
  • 範囲が限られた共通機能は デコレータ で実装する。


次回はテンプレートの応用的な使い方をやってみたいと思います。