Pyramidのコントローラースタイル

とりあえずコントローラースタイルと書いたが、ようするにWebアプリケーションがリクエストを受け取ってから処理に入るまでの流れである。 Pyramidはあえて複数の方法を採用している。その他のフレームワークから来る人たちがお気に入りの方法をとれるようにするためだ。 大きく分けて、Zope系由来のトラバーサル、DjangoやPylonsが使っているURLディスパッチがある。 (TurboGearsはPylons上のフレームワークだけどURLディスパッチをオミットしてトラバーサルっぽい動きをする) Pyramidはこの2種類をそれぞれ使えるし、同時に利用することもできる。 とりあえず2種類を説明して、そのあとに混合したパターンを説明しよう。

URLディスパッチ

最近のWebフレームワークはこちらが主流になっているようだ。 URLを正規表現やそれに近いパターンで解析して、パターンごとのコントローラーやビューを呼び出す。 リクエストからレスポンスまでの処理のは以下のようになる。

  • URLパターンマッチング
  • パターンに対応するビューの呼び出し
  • ビューの中でモデルをローディング
  • ビューがテンプレートにモデルを渡す

PyramidでのURLディスパッチ

Pyramidはルーティングとビューの登録がわかれている。

ルーティング:

config.add_route("user", "/users/{user_id}")

"user"ルートのURLパターンの登録である。 "{}" で囲まれている部分はプレースホルダーとなっていて、この部分に相当するパラメータは request.matchdict から取得できる。

ビュー登録:

@view_config(route_name="user", renderer="user.mako")
def user_view(request):
    user = User.query.filter(User.id==request.matchdict["user_id"]).one()
    return dict(user=user)

ビューを登録する方法はいくつかあるが、一番簡単であろうデコレータを使った方法である。 デコレータを複数使えば同じビューを異なるルートに登録できる。 また、そのときにテンプレートを別々に指定できる。

View Predicate

なぜルーティングとビュー登録がわかれているのか? Pyramidではビューを決定する情報はルートだけではないのである。 例えば、リクエストメソッドやXHR、特定のヘッダの有無などもビューを決定するための条件となる。 (もっとも重要な条件はトラバーサルと一緒に説明しよう)

ルーティングによる条件はさきほどの例にあるように route_name で指定される。 route_name="user" というのは "user"という名前のルーティングにマッチしたリクエストを条件とするという意味だ。 request_method="POST" とすれば、POSTメソッドの場合だけそのビューが呼ばれることとなるし、xhr=Trueとすればajaxリクエスト(XMLHttpRequest)の場合に呼び出されるという意味だ。

特に xhr プリディケイトは、ビューの切り替えだけでなくrendererの変更でも使われる:

@view_config(route_name="user", renderer="user.mako")
@view_config(route_name="user", renderer="json", xhr=True)
def user_view(request):
    user = User.query.filter(User.id==request.matchdict["user_id"]).one()
    return dict(user={"name": user.name, "type": user.user_type.name})

複数のview_configを使っているが上は通常のブラウザからのリクエストで、下はXHRによるリクエストだ。 同じルートに対して同じビューが呼び出されるが、それぞれのリクエストによって最終的なレスポンスボディが変化する。 通常のリクエストではuser.makoというテンプレートが指定されている。これはMakoTemplateエンジンで処理されて、HTMLとしてボディを返すだろう。 XHRの場合はrendererがjsonなので、ビューの戻り値のdictがそのままjson.dumpsされて返される。 処理自体は変わらないがリクエストの種類などに応じてレスポンスのフォーマットを変える良い例だ。

ルートURL

他のフレームワークではreverse_urlとも呼ばれる機能だが、パターンに値を埋め込んでURLを生成する仕組みである。 Pyramidでは、request.route_urlメソッドを使う。

トラバーサル

ではコントローラーのもう一方の方法であるトラバーサルである。 トラバーサルの場合はルートオブジェクトが最初に存在する。 このルートから、URLにそってオブジェクトツリーをたどっていくのがトラバーサルだ。 ツリーの末端に行くか、URLを消費しきったときに、このURLに対応したオブジェクトが決定される。 このオブジェクトをPyramidではcontextと呼んでいる。

オブジェクトのデフォルトビュー

さきほどの例と同じようなURLを考えてみよう:

/users/1

このようなURLが与えられた場合に、rootオブジェクトから以下のようにトラバーサルされる:

root['users']['1']

Zopeではもっと複雑な経路設定ができるが、Pyramidでは __getitem__ で辿っていくようになっている。 結果として、おそらく Userクラスのインスタンスなどが取得されるはずである。 このリクエストでのcontextが、このUserオブジェクトとなる。

次にPyramidは、Userオブジェクトに対応するビューを探し出す。 URLディスパッチでも説明したとおり、PyramidのビューはURLパターンだけに対応するものではない。 トラバーサルで重要なプリディケイトは、contextとname(route_nameではないことに注意)である。 contextは説明した通りUserオブジェクトである。ひとまずnameのことは忘れて、contextの条件だけを考えよう。

@view_config(context=User, renderer="user.pt")
def user_view(context, request):
    return dict(user=context)

この場合はview_configでcontextのクラスを指定する。 URLディスパッチではビューの中でモデルをローディングしたが、トラバーサルでは先にモデルが決定されてから、ビューが選択される。 そのためモデルはcontextとして、ビューの引数で渡されてくる。 モデルのメソッドを呼ばないのであれば、ビューは単にcontextとrendererを結びつけるだけの役割になる。

ビュー名

さて、トラバーサルではURLでコンテキストを決定してそれに合わせたビューが呼び出されるが、このままではコンテキストのクラスごとに1つのビューしか使えない。 トラバーサルでは、コンテキストが決定されたあとに残ったURLをビュー名として条件に使えるようになっている。 例えば、 "/users/1/edit" といったURLがあった場合、 "/users/1" に対応したuserオブジェクトが "edit" という名前で __getitem__ できなければ(__getitem__メソッドがなかったり、KeyErrorが発生したりなど) contextがuserオブジェクトとなり、"edit"がビュー名となる。

結果として、以下のような name="edit" という条件がついたビューが呼び出される:

@view_config(context=User, name="edit", renderer="user_edit.pt")
def user_edit_view(context, request):
    form = Form(context)
    if request.POST and form.validate(request.POST):
        .... do something
    return dict(user=context, form=form)

また、デフォルトではビュー名は "" 空文字として扱われている。

リソースURL

トラバーサルでたどるオブジェクトツリーのそれぞれのオブジェクトをリソースと呼ぶ。 contextは、リクエストURLによって一意に決定された特別なリソースである。 リソースはトラバーサル中にURLによって辿られるが、逆にリソースからURLの生成も可能である。 リソースからURLを生成するには、親の方向に向かってルートまでオブジェクトをたどり、オブジェクトの名前を "/" で連結していく。 Userオブジェクトから辿っていく場合を考えてみよう。

user,users,root の順に辿っていくが、この道筋はそれぞれのリソースの __parent__ 属性によってきめられている。 rootは __parent__ が Noneになっており、そこがトップレベルだという印となる。 また、オブジェクトごとの名前は __name__属性で取得される。 rootは無名、usersは"users"、userは"1"と名前を持っている場合、生成されるURLは "/users/1" となる。

URLディスパッチとトラバーサルの比較

トラバーサルの場合、URLに対応したオブジェクトが自然に決まる仕組みとなっていて、さらにモデルが自分のURLを知っているようになっている。 ただし、 __parent__や__name__の指定はプログラマが行わなければならない。 ZODBのようなオブジェクトツリーを意識したデータベースであれば自然な方法だが、 RDBMSのようにオブジェクトがテーブルにフラットに保存されている場合は、URLディスパッチを使う方が自然と思われる。

URLディスパッチとトラバーサルの混合

PyramidではURLディスパッチしてから、さらにトラバーサル処理を実行する手段が提供されている。 非常に高度な利用法となるが、それぞれの役割を制限して2つ同時に乗りこなしてみよう。

軽いURLディスパッチ + がっつりトラバーサル

トラバーサルを主体にした使い方である。 通常の "/" からルートオブジェクト以下にトラバーサルするものに付け加えて、 "/admin" など特定用途のプリフィックスからトラバーサルするものを併用する。

class User(object):
     def __init__(self, parent, id):
         self.id = id
         self.__name__ = str(id)
         self.__parent__ = parent

class UserRepository(dict):
     def __init__(self, parent):
         self.__parent__ = parent
         self.__name__ = 'users'

class Root(dict):
     __parent__ = __name__ = None

root = Root()
users = UserRepository(root)
root[users.__name__] = users
user = User(users, 1)
users[user.__name__] = user

def root_factory(request):
    return root

@view_config(context=User, renderer="user.pt")
def user_view(context, request):
    return dict(user=context)


@view_config(context=User, renderer="admin/user.pt", route_name="admin")
def admin_user_view(context, request):
    return dict(user=context)

@view_config(context=User, name="edit", renderer="admin/edit_user.pt", route_name="admin")
def admin_user_edit(context, request):
    return dict(user=context)

config = Configurator(root_factory=root_factory)

config.add_route("admin", "/admin/*traversal")
config.scan(".")

この場合、 "/users/1" であれば、userがcontextとなり、すでに説明したように context=Userを条件としている user_view が呼び出される。 "/admin/users/1" の場合は "admin" ルートからトラバーサルを実行するため、context=Userかつroute_name="admin"を条件としている admin_user_view が呼び出される。 admin_user_editは、 "/users/1/edit"では呼び出されず、 "/admin/users/1/edit" の場合だけ使えるようになる。 基本はトラバーサルを使いつつ、性格の異なるサイトを同じオブジェクトツリーで構成する場合に使える手段である。

ガッツリURLディスパッチ + 軽くトラバーサル

次はURLディスパッチを主体とした方法である。 URLディスパッチで "/users/{user_id}" までを特定して、その後にビュー名を得る部分だけトラバーサルを利用する。 この場合はルートにfactoryで、トラバーサルの主体を渡さなければならない。

class User(object):
     def __init__(self, id):
         self.id = id

class UserRepository(dict):
     pass

users = UserRepository(root)
user = User("1")
users[user.id] = user


def user_factory(request):
    return users.get(request.matchdict['user_id'])

@view_config(route_name="user", renderer="user.pt")
def user_detai_view(context, request):
    return dict(user=context)

@view_config(route_name="user", name="detail", renderer="user_detail.pt")
def user_view(context, request):
    return dict(user=context)


config = Configurator(root_factory=root_factory)

config.add_route("user", "/users/{user_id}/*traversal", factory=user_factory)
config.scan(".")

"/users/1" は、"user" ルートにマッチして、ビュー名がないため、user_view が呼び出される。 "/users/1/detail" の場合、 "user"ルートにマッチして、さらに "detai" というビュー名を持つため user_detail_viewが呼び出される。

ある特定URLとそれ以下のパス名で扱われるオブジェクトが同じ場合は、このようにビュー名だけでビューを切り替えることが可能である。 末端のパスが1か所だけ違うルートが大量に定義されている場合は利用を検討してみてはどうだろう。

まとめ

ということで、Pyramidが持つ柔軟性とハードルの高さの責任の有力候補である二種類のコントローラースタイルとその混合方法について自分なりに整理してみました。 ここまで必要かどうかって思うかもしれませんが、ぜひ試してみて、これまでいまいちうまく書けないという部分が多少でも解決できれば、きっと僕のところにシュークリームが舞い込んでくることと思います。

Comments !

blogroll

social