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」に委譲されてしまっています。ここまで単純だと、切り替え的な処理は全くやってい無さそうです。
読み取れるのは、
- クライアントからリクエストを受けた時に、トランザクション管理下に入る。
- 例外が発生したときに、トランザクションが dirty (何か処理されている)状態だったらロールバックする。そして、トランザクション管理下から離れる。
- 正常に処理がされていて、なおかつトランザクション管理下で処理が行われていたらコミットする。トランザクション管理下から離れる。
上記のことです。ということで、何かしら関数ごとの処理分けがされていることを期待してたのに、Django のトランザクション管理では、「処理分けは成功失敗のみ」で、「トランザクション機能の有効無効はMiddlewareのON/OFFに完全依存」していることが分かりました。
ちなみに
transaction.enter_transaction_management() transaction.managed(True)
で何をやっているかと言うと、最初に呼び出す関数で、リクエスト(Thread)が使用するコネクションをThreadローカル変数に保存して、次の関数でコネクションを初期化しています。
コネクションの初期化タイミングに違和感がある実装で、本当にDjangoは大丈夫なんだろうかと不安になったのは内緒です。
TransactionMiddlewareには期待できない
使えないっていう話ではなくて、期待してた実装は行っていないようです。
途方に暮れてしまうところでしたが、よさそうなのを見つけました。
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 に実装する。
- 範囲が限られた共通機能は デコレータ で実装する。
次回はテンプレートの応用的な使い方をやってみたいと思います。