銀月の符号

Python 使い見習いの日記・雑記

URL のクエリ要素にあえて情報をもたせる必要はない?

ページ番号などの情報を URL に持たせる時の話。 Django 的には /something/?page=7 のようにクエリ要素を使うのと /something/7/ のような URL はどちらが便利なのか考えてみた。

今の時点での結論は「URL ディスパッチャが強力なので /something/7/ でよい」かな。 Django 使いはじめたばかりなのですぐに意見が変わる可能性は大。

/somethig/7/ だけでなく /something7/, /something/page7/, /something/page/7/ など、正規表現しだいでいろいろやりようがあるのもメリット。

以下比較。

URL ディスパッチャの記述

ここだけはクエリ要素使用のほうが楽。

クエリ要素に情報を持たせる場合

URL ディスパッチャはクエリを無視するのでやることがない。以下の記述で /something/, /something/?page=7 いずれのアクセスも index ビューに結び付けられる。

from django.conf.urls.defaults import *

urlpatterns = patterns('myapp.views',
    url(r'^something/$', 'index'),
)
パスに情報を持たせる場合

正規表現で URL から値を取り出す。正規表現しだいでいろいろできるのがよい感じ。以下の記述で /something/ へのアクセスは index(request) という呼び出しに, /something/7/ は index(request, page=7) になる。

from django.conf.urls.defaults import *

urlpatterns = patterns('myapp.views',
    url(r'^something/(?<page>\d+)/$', 'index', name='index-pagenated'),
    url(r'^something/$', 'index', name='index'),
)

view 関数の記述

手間は大差なし。可読性は view に引数があるパス版のほうが少しだけよい気はする。でも docstring 書かないと将来わけわからなくなるのはどちらも同じ。

クエリ要素に情報を持たせる場合

値を取り出すのはビューの役目となる。request.GET から取り出す。

from django.http import Http404

def index(request):
    page = request.GET.get('page', 1)
    try:
        page = int(page)
    except ValueError:
        raise Http404
    # 以下略
パスに情報を持たせる場合

値を取り出すのは URL ディスパッチャの役目なので view には値を受け取るための引数を用意しておく。

from django.http import Http404

def index(request, page=1):
    try:
        page = int(page)
    except ValueError:
        raise Http404
    # 以下略

URL の逆引き(テンプレート版)

実際にはページ番号などの情報はコンテキストから持ってくるんだろうけれど、とりあえず "7" リテラルでハードコーディング。

パス版のほうが分かりやすいと思う。 url タグが全部面倒を見てくれるので一安心。

一方、クエリ版はやっかい。逆引きは URL ディスパッチャの設定を元に行われる。このため、ディスパッチャが関与していないクエリ部分は自動生成できない。そして自前で埋め込もうとすると非 ASCII 文字列が含まれる場合、問題が発生する。 HTML 内に UTF-8 でそのまま現れるため URL の体をなさなくなる。これを解決するためには urlencode フィルタを通す。

クエリ要素に情報を持たせる場合
<p>
  <a href="{% url myapp.views.index %}?page=7">
    ページ7
  </a>
</p>

非 ASCII 文字に備えるにはこう。今回は単なる ASCII 文字の "7" なので余計なのだけれども。

<p>
  <a href="{% url myapp.views.index %}?page={{ "7"|urlencode }}">
    ページ7
  </a>
</p>
パスに情報を持たせる場合

シンプルでよい感じ。

<p><a href="{% url index-pagenated page='7' %}">ページ7</a></p>

URL の逆引き(Python コード版)

やはりパス版のほうが勝っていると思う。

クエリ要素に情報を持たせる場合
from django.core.urlresolvers import reverse

url = reverse('myapp.view.index') + '?page=%s' % '7'

非 ASCII 文字に備えるにはこうか?

from django.core.urlresolvers import reverse
from django.utils.encoding import iri_to_uri

url = iri_to_uri(reverse('myapp.view.index') + u'?page=%s' % u'7')

urllib.urlencode の Django 版である django.utils.http.urlencode を使うと、非 ASCII 文字に備えつつ、コードをスマートにできる。辞書でクエリを表現して urlencode に渡せばよい。値が非 ASCII 文字を含むユニコード文字列でも問題なし。

from django.core.urlresolvers import reverse
from django.utils.http import urlencode

query = urlencode(dict(page=u'7'))
url = ''.join([
        reverse('myapp.view.index'),
        '?',
        query])
パスに情報を持たせる場合

単純明快。

from django.core.urlresolvers import reverse

url = reverse('index-pagenated', kwargs=dict(page=u'7'))

メモ、ページ分割をしたいだけならば

ページ分割を実現するためにはビューの以下略の部分を実装する必要がありそうだが、 Django には paginator が用意されている。これをつかえばよい。

さらには 汎用ビュー django.views.generic.list_detail.object_list だけでも十分だったりする。内部で paginator を用意してくれるので1ページに何件表示するか引数で指示を与えるだけでビューの作成は終わってしまう。あとはテンプレートの作成をがんばるだけ。