unoh.github.com

DjangoのURL逆引き機能

Wed Oct 03 09:07:31 -0700 2007

Chihiroです。

今回は、Django 0.96以降から改善が進んでいるDjangoのURL逆引き機能に関して紹介したいと思います。この機能を上手く使うと、Djangoを使ったWeb開発において、URLの設計と維持にかかる手間を大幅に削減できます。

公式ドキュメントの日本語訳にもこの機能の説明はありますが、説明が分散していて全体像が見えにくいと思いますので、この機能を使わない場合の問題点を指摘し、次に使った場合、ルーティング、モデル、ビュー、テンプレートがそれぞれどのように書けるかを説明したいと思います。

なお、動作確認はSubversionから取得した開発版Django(rev.6447)を使用して行っています。

Django0.96以前のURL定義の問題点

Djangoの大きな特徴として、「自由なURL設計」がありますが、一方でモデル、テンプレート、ビュー、ルーティングにおいて冗長にURLの定義しなくてはいけないという問題点がありました。

例えば、

http://example.com/entries/1978/

というURLで指示されるリソース(例えばブログのエントリ)を考えた場合、まずurls.pyに記述するルーティング定義(URLConf)に、次のような設定を行う必要があります。

patterns += ('myaproject.myapp.views',
             (r'^entries/(?P<entry_id>\d+)/', 'edit_entry'),
             )

次に、Adminツールなどの恩恵を受けるために、Djangoの流儀に従って、次のような感じでモデルにget_abosolute_urlメソッドを実装していました。

from django.db import models

class Entry(models.Model):
    title = models.CharField(max_length=250)
    content = models.TextField()

    def get_absolute_url(self):
      return '/entries/%s/' % self.id

    def __unicode__(self):
        return self.title

URLConfで書いたr'^entries/(?P<entry_id>\d+)/'は正規表現で、モデルに書いたのは'/entries/%s/'というフォーマット文字列という違いはありますが、似たような定義を繰り返しています。

さらに、リソースを参照したり、リソースに対して何らかの操作を行ったりする場合、そのためのURLのフォーマットをテンプレート内に直接書く必要があります。

例えば、あるブログのエントリの編集画面のURLを"/entries/{entry_id}/edit/"のように設計した場合、テンプレートの中に次のように書くことになるでしょう。

{% if entries %}
<ul>
  {% for entry in entries %}
  <li>{{ entry.title|escape }}<a href="/entries/{{ entry.id }}/edit/">{% trans "Edit" %}</a></li>

  {% endfor %}
</ul>
{% endif %}

ここまでの例で分かるように、モデル、ルーティング、テンプレートにおいて、"^entries/(?P<entry_id>\d+)/"、"/entries/%s/"、 "/entries/{{ entry.id }}/edit/"のように、似たようなURLの定義を繰り返す必要がありました。

URL定義のDRY化

開発版Djangoでは、URLConfでのURL定義を元に、ビュー関数名からリソースのURLの解決を行えるようになりました。分かりやすく言うと、urls.pyにURL設計をまとめることができるようになったのです。

基本的なCRUDを例にとって説明します。

まず、URLConfは次のように定義できます。

# -*- coding: utf-8 -*-
# myproject/urls.py
from django.conf.urls.defaults import *
from myapp.models import Entry

urlpatterns = patterns(
    'myproject.myapp.views',

    # エントリの新規作成
    url(r'^entries/new/$',
        'create_entry',
        name='create_entry'),

    # エントリの編集
    url(r'^entries/(?P<entry_id>\d+)/edit/$',
        'edit_entry',
        name='edit_entry'),

    # エントリの削除
    url(r'^entries/(?P<entry_id>\d+)/delete/$',
        'delete_entry',
        name='delete_entry'),
)

urlpatterns += patterns(
    'django.views.generic.list_detail',

    # エントリ一覧
    # GenericViewを使います
    url(r'^entries/$',
        'object_list',
        dict(queryset=Entry.objects.all(),
             allow_empty=True),
        name='show_entries'

        ),

    # エントリ詳細
    # GenericViewを使います
    url(r'^entries/(?P<object_id>\d+)/$',
        'object_detail',
        dict(queryset=Entry.objects.all()),
        name='show_entry_detail'
        ),
    )

urlpatterns += patterns(
    'django.views.generic.simple',

    # 上のどれでもない場合は/entries/にリダイレクト
    url(r'',
        'redirect_to',
        dict(url='/entries/')
        ),

    )

上のコード例にあるように、開発版Djangoでは、今までのタプル(シーケンス)でのURL定義の他に、新たにurlという関数で定義を行えるようになりました。この関数を使うと、名前つきのURLパターンを定義するのが多少楽になります。

名前つきをURLパターンを使った方が、URL定義の逆引きをするときにタイプ量が減るという単純なメリットもありますが、名前付きURLを使わないと解決できないケースもあります。詳しくは公式のドキュメントを参照してください。

開発版Djangoの機能を活用すると、URLの設計(URLのフォーマットを決める作業)はこのURLConfの中で完結し、モデル・ビュー・テンプレートでは、URLパターンの名称とモデルのキーなどのパラメータからURLを決定することができるようになります。

まず、モデルでは、permalinkデコレータ関数を使うと、URLConfに基づいてモデルのURLを構成することができます。(公式ドキュメントによる解説)

from django.db import models
from django.db.models import permalink

class Entry(models.Model):
    title = models.CharField(max_length=250)
    content = models.TextField()

    @permalink
    def get_absolute_url(self):
        return ('show_entry_detail', [str(self.id)])

    def __unicode__(self):
        return self.title

ビュー関数からURLの解決を行う場合には、reverse関数を使います。(公式ドキュメントによる解説)

# -*- coding: utf-8 -*-
from django.shortcuts import *
from django.http import *
from django.core.urlresolvers import reverse
import django.newforms as forms

from models import Entry

EntryForm = forms.models.form_for_model(Entry)

def create_entry(request):
    """
    新しいエントリを作成する。
    作成後は、エントリ一覧のページにリダイレクトする。
    """
    form = EntryForm(request.POST or None)
    if form.is_valid():
        entry = form.save()
        return HttpResponseRedirect(reverse('show_entries'))

    return render_to_response('myapp/entry_form.html', {'form': form})

テンプレート内からURLを解決する場合は、{% url %}タグを使います。(公式ドキュメントによる解説)

このタグは、{% url URLパターンの名前 パラメータ %}のように使います。

{% load i18n %}
{% extends "base.html" %}
{% block content %}
<div>
  <a href="{% url create_entry %}">{% trans "Post a new entry" %}</a>
</div>

{% if object_list %}
  <ul>
  {% for entry in object_list %}
    <li>{{ entry.title|escape }}
        (<a href="{% url edit_entry entry_id=entry.id %}">{% trans "Edit" %}</a> |
         <a href="{% url delete_entry entry_id=entry.id %}">{% trans "Delete" %}</a>)
    </li>
  {% endfor %}
  </li>
{% else %}
  <p>{% trans "No entries" %}</p>
{% endif %}
{% endblock %}

まとめ

permalink, reverse, {% url %}を使うと、URL設計を変更したい場合などに、変更にかかるコストを大幅に削減できます。またURL設計をURLConfに集約できるので、コードやテンプレートの可読性も高まります。

例えば、「当初、"/entries/{entry_id}/"のようなURLをブログ・エントリのパーマリンクとしていたが、開発段階でパーマリンクを"{entry_id}/"に変更することになった」というケースを考えてみてください。この場合でも、permalink, reverse, {% url %}を使っていれば、URLConfを書き換えるだけで済みます。

今回のコード例は下記のリンクからダウンロードできますので、どうぞご利用ください。

django071003.tgz