読者です 読者をやめる 読者になる 読者になる

orangain flavor

じっくりコトコト煮込んだみかん2。知らないことを知りたい。

DjangoでのURL⇔view関数の正引き・逆引き

DjangoのURLディスパッチャは正引き(URLからview関数への変換)は比較的単純ですが、逆引き(view関数からURLへの変換)はちょっとわかりにくいです。get_absolute_urlメソッド, permalinkデコレータ, urlタグ, reverse関数, url関数, 名前付きURLパターンなど、キーワードはよく目にするけれど、具体的にどうすればいいのかよくわからないということはないでしょうか?

Djangoのドキュメントでは正引き・逆引きという視点からのまとまった解説はないので、ここで解説したいと思います。

正引き(URLからview関数への変換)

正引きと言うのはURLからview関数への変換のことです。urls.pyに正規表現とそれに対応するview関数名の組(URLパターン)を定義することで実現しています。その目的のためだけならURLパターンに名前をつける必要はないのですが、後述する逆引きのためには名前があると都合がいい*1ので、名前付きパターンというものが存在します。正引きについて重要なのは名前なしパターンと名前付きパターンの違いだけです。

urlpatterns = patterns('',
    # 名前なしパターン
    (r'^posts/(?P<post_id>\d+)/$', 'posts.views.detail'),
    # 名前付きパターン
    url(r'^posts/(?P<post_id>\d+)/$', 'posts.views.detail',
        name='posts-detail'),
    ...
)

このように普通にタプルを指定すると名前なしパターンになり、url関数を使ってnameパラメータを指定すると名前付きパターンになるようにドキュメントには書いてあるのですが

    # 名前付きパターン
    (r'^posts/(?P<post_id>\d+)/$', 'posts.views.detail',
        None, 'posts-detail'),

としても名前付きパターンを作れます。当たり前ですが、

    # 名前なしパターン
    url(r'^posts/(?P<post_id>\d+)/$', 'posts.views.detail'),

とすれば、名前なしパターンになります。

ドキュメントでは、url関数と名前付きパターンを関連付けて説明しているのでわかりにくくなっていると思います。確かに主にこの形で使うのですが、url関数の存在意義は、タプルだと途中の引数を省略するのが面倒だけど、関数なら名前付き引数を利用できて、省略するのが簡単になるというところなのです。django/conf/urls/defaults.pyのpatterns関数の中でやっていることを見れば、タプルの指定はそのタプルを引数としてのurl関数の呼び出しとほぼ同じであることがわかると思います。

def patterns(prefix, *args):
    pattern_list = []
    for t in args:
        if isinstance(t, (list, tuple)):
            # タプル(リスト)のとき
            #  →そのタプルを引数としてurl関数を呼び出す
            t = url(prefix=prefix, *t) 
        elif isinstance(t, RegexURLPattern):
            # url関数のとき(url関数はRegexURLPatternを返す)
            #  →prefixを追加するけどそのまま
            t.add_prefix(prefix)
        pattern_list.append(t)
    return pattern_list

ちょっと深く入り込みすぎた気がしますが、ポイントは、名前なしパターンと名前付きパターンの二つがあり、それをつくるのはタプルでもurl関数でも可能だということです。

逆引き(view関数からURLへの変換)

逆引きと言うのはview関数からURLへの変換です。それまでSymfonyを使っていた自分には非常に驚きだったのですが、この機能はVer.0.96で初めて導入されました。それまではURLを直書きしていたのです。というか今でも直書きは使われているんじゃないかと思います(少なくとも私は使っています)。URLの設計がきちんとされていれば直書きも意外と使いやすいです。

話がそれましたが、直書きと逆引きという、2種類のURL逆参照の方法を対比させて説明していきます。直書きを逆参照と呼ぶのはおかしいと言われるかもしれませんが、ここでは直書きと逆引きをあわせて逆参照と呼ぶことにします。なお、説明の際には以下のパターンを使用します。

url(r'^posts/(?P<post_id>\d+)/$', 'posts.views.detail',
    name='posts-detail')

まず、URLが必要になる局面は主に2つあります。1つはview関数内でHttpResponseRedirectを使ってリダイレクトするときであり、もう1つはテンプレート内でaタグを使ってリンクを張るときです。もちろんこれだけではありませんが、view関数内とテンプレート内という2つの場面があるということが重要です。

view関数内では

from django.core.urlresolvers import reverse

def submit(request):
    ...

    # 直書き
    return HttpResponseRedirect('/posts/%i/' % post.id)
    
    # 逆引き(ビュー関数名から)
    return HttpResponseRedirect(
        reverse('posts.views.detail', args=[post.id]))
    # 逆引き(ビュー関数への参照から)
    return HttpResponseRedirect(
        reverse(detail, args=[post.id]))
    # 逆引き(URLパターン名から)
    return HttpResponseRedirect(
        reverse('posts-detail', args=[post.id]))

として、逆参照を実現します。直書きはまあそのまんまですが、逆引きするときにはreverse関数を使います。ビューの指定にはビュー関数名・ビュー関数への参照・URLパターン名のいずれかを使えます。ちなみにreverseの引数指定にはargsだけでなくkwargsを使ってキーワード引数を渡すこともできます。

一方テンプレート内では

{# 直書き #}
<a href="/posts/{{ post.id }}/">{{ post }}</a>

{# 逆引き(ビュー関数名から) #}
<a href="{% url posts.views.detail post.id %}">{{ post }}</a>
{# 逆引き(URLパターン名から) #}
<a href="{% url posts-detail post.id %}">{{ post }}</a>

という風に逆参照します。直書きは相変わらずそのままですが、逆引きするときには{% url %}タグを使います。なお、パラメータはカンマで区切って複数渡すことができます。

さて、基本的にはこれだけなんですが、モデルからそのモデルの詳細ページのURLを取得したいことがあります。そこで使用するのがモデルのget_absolute_urlメソッドです。ただのURLを取得する関数なので他の名前でもいいんですが、get_absolute_urlにしておくと、adminとかsyndicationとかでいいことがあるので、この名前を使うといいと思います。

直書きの場合は

class Post(models.Model):
    ...
    def get_absolute_url():
        return '/posts/%i/' % self.id

というようにURLを返すメソッドを定義します。
逆引きするときは

from django.db.models import permalink

class Post(models.Model):
    ...
    @permalink
    def get_absolute_url():
        # 逆引き(ビュー関数名から)
        return ('posts.views.detail', [str(self.id)])
        # 逆引き(URLパターン名から)
        return ('posts-detail', [str(self.id)])

とすればよいです。ポイントはpermalinkデコレータです。(ビュー関数, 引数, キーワード引数)というタプルを返す関数に、permalinkデコレータを付加すると、内部でreverseしてURLを返すようにしてくれます。まあ正直permalinkデコレータはreverseを呼び出すだけで、無駄にややこしくしてるとしか思えませんが…。Djangoはこの方法を推奨しているようですが、自分でreverseを呼び出してもかまわないと思います。

この関数を使うときはビュー内では

    return HttpResponseRedirect(post.get_absolute_url())

とし、テンプレート内では

<a href="{{ post.get_absolute_url }}">{{ post }}</a>

とします。

ちなみに、get_absolute_urlを定義すると、URLの設定があちこちのモデルに散らかってよろしくないと思う場合は、settings.pyにABSOLUTE_URL_OVERRIDESを書けば、まとめることができます。


長々と説明して来ましたが、逆引きはいろいろとややこしいということをわかってもらえたでしょうか?ここまで解説しておいてあれなんですが、{% url %}タグのドキュメントには今のところ*2Note that the syntax for this tag may change in the future, as we make it more robust.と書いてあるので、これから変更される可能性があります。他にも逆引きや名前付きパターンは出来たばかりで、使いにくいところがあるのでこれから変更されるんじゃないかと勝手に思っています。

*1:ビュー関数のパスに依存しなくて済む/複数のパターンが同じビューを呼び出すときでも特定のパターンを指定できる/タイプ量が少なくなるなど

*2:1.0beta1が出たぐらいの時点