Djangoにおいて重要かつ複雑そうに見えて実はそれほど難しくないのが、デフォルトではurls.py
に設定されている項目です。
この記事ではurls.pyに設定されている内容について解説してきます。
urls.py
に記載する内容のことを便宜上urls.py
とファイル名で呼んでいます
urls.pyの役割
urls.py
はクラスベースビューはもちろん関数で作成されたViewとURLを連携させる役割を持っています。
Djangoはウェブアプリケーションフレームワークですので、ウェブサイトを構築します。
構築されたウェブサイトにはURLが設定されていますよね。そのURLに応じてViewを呼び出します。
どのURLに対してどのViewを呼び出すかをurls.py
に定義します。
それがurls.py
の役割です。
例えば、
https://foo.com/
にアクセスされたとき、ArticleTemplateView
を実行するhttps://foo.com/blog/
にアクセスされたときはArticleListView
を実行する
といったURLとの連携をさせる、ということです。
次の項から実際に書き方を通じて理解を深めていきます。
urls.pyはURLとViewを連携させるための定義を記述する
モデルとViewの定義
urls.py はURLとViewを連携させるため、あらかじめViewを定義しておく必要があります。
Viewを定義するにはモデルが必要です。
ということで、ここでは以下のようなモデルとViewを定義します。
モデルはこんな感じだと思ってください。
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
Viewはこんな感じです。
from django.views.generic import ListView
from .models import Article
class ArticleListView(ListView):
model = Article
このような定義がされているとして、以下の解説に進みます。
ListView
については以下の記事を参考にしてください。
urls.pyの書き方
では実際に記述していきます。
Django 3.2 で記述しています。
プロジェクトを作成した直後のurls.py
はこのようになっているはずです。
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
ここに先ほどのViewを追加して、その後解説を加えていきます。
from django.contrib import admin
from django.urls import path
from bar.views import ArticleListView # インポート
urlpatterns = [
path('articles/', ArticleListView.as_view()), # この行を追加
path('admin/', admin.site.urls),
]
4行目と7行目を追加しました。
4行目は作成したArticleListView
をインポートしています。
7行目が定義部分です。
path('articles/', ArticleListView.as_view()),
この部分ですね。
path関数
path関数はURLとViewを連携させる関数です。
定義のしかたは以下の通りです。
path(連携させるURL, 対応するView)
path関数の第一引数は連携させるURLを指定します。
今回はarticles/
としています。
http://foo.com/ のようなドメイン部分は?と思われるかもしれませんが、指定の必要はありません。
そこより下位のURLを指定してください。
ArticleListView.as_view()
を指定しています。この指定によってURLとViewが連携し https://foo.com/articles/
にアクセスされると、ArticleListView
が動作してページを表示します。
これがURLとViewを連携させる、ということです。
urlpatternsとは
urls.py
で重要な部分は2つあります。
一つはpath
関数であり、もうひとつはurlpatterns
という変数です。
この変数はpath関数のリストになっていて、path関数によって連携を構築した設定を格納します。
しかし、リストであることは変わりありませんので、path関数を後で追加することも可能です。
意味が分からないと思いますので、コードで示します。
from django.contrib import admin
from django.urls import path
from bar.views import ArticleListView
urlpatterns = [
path('admin/', admin.site.urls),
]
urlpatterns += [
path('articles/', ArticleListView.as_view()),
]
このようにして、はじめに定義したurlpatterns
に対して追加でpath関数のリストを追加することができます。
urls.pyの書き方が変わったことについて(Django 2.0より)
urls.py
の書き方は Django 2.0 から大きく変更されました。
Django 1.x の時代は url
関数で正規表現を使ってアクセスされたURLを定義しましたが、Django 2.xからはもう少しわかりやすい表記に変更されています。Django 1.x 時代のurl
関数はre_path
という関数で実行可能です。
いまどきDjango 1.x 系を使用しているプロジェクトは多くないでしょうが、注意してください。
関数のViewをurls.pyに指定する
ネットの情報などを検索すると、関数でViewを記述している例もたくさん見つかります。
公式ドキュメントのチュートリアルでも使用されています。
例えば、以下のようなViewです。
def sample(request):
return HttpResponse('Hello')
このようなViewは今でも記述されているメジャーな記述方法です。
ほとんどの場合クラスベースビューで対応できますが、それだとややこしくなってしまう場合などに使用します。
こういった関数のViewをurls.py
に定義するときは、以下のようにします。
path('sample-url/', sample),
ポイントとしては、関数で作成したViewの場合、呼び出してはならない、ということです。
カッコを記述してはいけない、ということですね。
カッコを記述しているとエラーになりますので注意してください。
関数のViewをpath関数に記述するときは、Viewの関数を呼び出してはならない(カッコを書いてはいけない)
個別ページのurls.py
例としてListViewを記述しましたが、個別ページのURLは記述がすこし違います。
今回は例としてDetailView
を取り上げてみます。単一のページを扱うViewであればほとんどのクラスベースビューに該当する内容です。
Viewに記述を追加しましょう。
こんな感じです。
from django.views.generic import ListView, DetailView # DetailView を追加
from .models import Article
class ArticleListView(ListView):
model = Article
# 以下を追記
class ArticleDetailView(DetailView):
model = Article
DetailView
を追加しました。
DetailView
については以下の記事を参考にしてください。
DetailView
の特徴としては、単一の記事を取得して表示します。
それを踏まえて、urls.py
を記述してみましょう。
urlpatterns
などは上記と変わりありませんので、path
関数部分だけ解説します。
path('<int:pk>/', ArticleDetailView.as_view()),
このように記述します。
注目すべきはURLの部分で、<int:pk>
と定義している部分です。
これは、URLのこの部分に数字が入り、さらにそれをpk
という変数に格納する、という意味になります。
2つの意味がありますので1つずつ説明していきます。
URLに数字が入るとは
これは単純にこういったURLに反応する、ということです。
https://foo.com/1/
このように<int:pk>
部分に数字が入る、という定義の仕方です。
ここには、数字であればどのような数でも入ります。ですので、100でも1000でもOKです。
逆に、1〜99までにしたい、といったニーズには応えられません。
そういった場合はre_path
を使用して正規表現で指定します。
また、仕様上0
ということはあり得ません。なぜあり得ないのかについては後の項で説明します。
pkという変数に格納するとは
クラスベースビューでは内部でpk
という変数を保持しています。
URLに<int:pk>
のよう定義があるときは、その値を自動的に取得してクラスベースビューが保持している変数pk
に代入します。
例えば、
https://foo.com/1/
のようなURLにアクセスされると1
が指定されたViewに渡されます。
クラスベースビューが受け取った場合は自動的に変数に格納され、自身の動作に使用されます。
関数で作成されたViewであれば、第二引数に渡されます。関数を作成する際に第二引数を記述して使用します。
def sample2(request, pk):
return HttpResponse('hello 1')
のように定義すれば、pk
に1
が代入されます。
urls.pyの仕組み
前項ではURLで取得した値がpk
に代入される事を説明しました。
では、なぜそのような動作をするのでしょうか?
これには前提知識が必要となりますので、そこから解説していきます。
データベースのレコードとプライマリーキー
このお話をするにはデータベースの基礎知識が必要になります。
データベースとモデルは密接な関係がありますので、まずはモデルを示したいと思います。上の記事で示したモデルと同じです。
モデルはこんな感じだと思ってください。
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
Djangoはこのモデルを通してデータを保存します。
さて、「データベースに保存されたデータ」というとき、どのようなイメージを描くでしょうか。
イメージの描き方はひとりひとり違うでしょうが、私はこのようなイメージを描きます。
ID | title | content |
1 | sample 1 | sample 1 content |
2 | sample 2 | sample 2 content |
Excelの表みたいですね。
モデルに定義されているフィールド「title」や「content」が保存されていますね。データが保存されていくたびに1行1行増えていきます。
ここで重要な部分は、モデルには「ID」というフィールドを定義していないにもかかわらずデータが保存されています。
実は「ID」はモデルに定義しなくても自動で設定されるフィールドです。
このフィールドはデータが保存されていくたびに1づつ値が増加していくフィールドです。
また、データが削除されても同じ値が使用されることはなく欠番となります。
例えば、
ID | title | content |
1 | sample 1 | sample 1 content |
2 | sample 2 | sample 2 content |
3 | sample 3 | sample 3 content |
4 | sample 4 | sample 4 content |
5 | sample 5 | sample 5 content |
「1, 2, 3, 4, 5」とデータが増えた後に、「3」を削除したとします。
ID | title | content |
1 | sample 1 | sample 1 content |
2 | sample 2 | sample 2 content |
4 | sample 4 | sample 4 content |
5 | sample 5 | sample 5 content |
そうするとこのような状態になりますね。
ここでもうひとつデータを追加するとどうなるでしょうか。
ID | title | content |
1 | sample 1 | sample 1 content |
2 | sample 2 | sample 2 content |
4 | sample 4 | sample 4 content |
5 | sample 5 | sample 5 content |
6 | sample 6 | sample 6 content |
このように「3」は欠番となり、あらたなデータは「6」として保存されます。
この番号は「1」から始まりますので「0」であることはありません。
この「ID」ですが、データベース的にはプライマリーキーとなっています。
つまり、この番号が分かればどのデータであるのか必ず識別できる、という意味です。日本語では「主キー」などと呼ばれます。
このプライマリーキーを使ってDjangoはデータを取得します。pk
とはプライマリーキーの略語です。
ここまでがDjangoとデータベースの関係で必要な前提知識です。
クラスベースビューがデータを取得するとき
さて、この項は前項で説明したプライマリーキーがクラスベースビューでどのように使用されているか、です。
path
関数に<int:pk>
が定義され、そのURL、例えばhttps://foo.com/1/
にアクセスがあった場合クラスベースビューは<int:pk>
に相当する部分を取得して変数pk
に保存します。<int:pk>
ですのでpk
です。
今回の場合はhttps://foo.com/1/
ですので、「1」が取得した数値であり、クラスベースビューが変数に保存する値です。
クラスベースビューはpk
に保存された値をプライマリーキーとして使用し、連携しているモデルのデータを読み出します。pk
に保存されたデータをデータベースから探索し、発見したらそのレコードを取得します。
取得されたレコードは変数object
に格納されてテンプレートに渡されます。
つまり、
https://foo.com/1/
であればIDが1のデータを取得https://foo.com/3/
であればIDが3のデータを取得
する、という動作をします。
以下に図で示します。下手ですみません。
データベースに保存されているID(プライマリーキー)は1から始まりますので、0は存在しません。
仕様上0が存在しない、というのはそういった理由です。
urls.pyに定義されたURLを生成する
見出しだけだと何のことか分からないかもしれません。
いままでクラスベースビューがURLの一部を取得する、という内容を解説しました。
今回は逆に、urls.py
に定義されたURLをテンプレートで生成しよう、という内容です。
ListViewで生成する
まずは、上記のArticleListView
で試してみましょう。
path('articles/', ArticleListView.as_view()),
このように定義していました。
ここにname
を追記します。
path('articles/', ArticleListView.as_view(), name='index'),
このname
は今回重要な役目を担っています。
まずは、解説なしでテンプレートの記述をしてみたいと思います。もちろん次の節で解説します。
シンプルにするため、URLを生成する部分だけ記述します。
{% url 'index' %}
このようにすれば、/articles/
というURLが生成されます。
もちろんこれでは役に立たないので、以下のように使用します。
<a href="{% url 'index' %}">記事一覧</a>
これでリンクが作成できますね。
テンプレートタグ url
今回、テンプレートタグ {% url %}
を使用しています。
これは、urls.py
から実際のURLを生成するテンプレートタグです。
path
関数に追加したname
を元にURLを生成する、という機能を持っています。
{% url %} は URLを生成する
このテンプレートタグ {% url %}
は以下のような構文です。
{% url 'nameに指定された名前' 引数1 引数2 ... %}
テンプレートタグはコンマではなく半角スペースで引数を設定します。
最初の引数はpath
関数に定義してあるname
の名前を記述します。
文字列ですのでシングルクォーテーションかダブルクォーテーションで囲います。
引数が必要な場合は続けて引数を設定していきます。必要なければ記述する必要はありません。
引数はテンプレートで使用できる変数でもOKです。
ArticleListView
では引数を使用しませんでしたので、後の記事で使用例を含めて解説します。
繰り返しですが今回は path
関数が以下の通りになってす。
path('articles/', ArticleListView.as_view(), name='index'),
ですので、name
に設定されているindex
をテンプレートタグurl
に指定することで、path
関数の第一引数に定義されているURLarticles/
が生成されました。
最初のスラッシュは絶対URLの指定ですね。path
関数では相対URLでの指定になっていますが、出力は絶対URLとなっています。この辺はDjangoがうまく処理してくれているところです。
URLを生成するメリット
大きなメリットは、URLを変更したいときにurls.py
を修正するだけでよい事です。
現在の定義は以下の通りです。
path('articles/', ArticleListView.as_view(), name='index'),
もし、URLを変更したくなった場合、ここの定義を変更するだけで済みます。
path('articles_test/', ArticleListView.as_view(), name='index'),
こんなURLはあり得ませんが、この部分を変更すれば
{% url 'index' %}
のように生成されたURLはすべて変更されます。
URLを変更すること自体良くないことなのですが、変更漏れがなくなります。
DetaiViewなどの個別ページのURLを生成する
ListView
のようなひとつのページのリンクを生成する方法は分かりました。
ではDetailView
のように、その都度違うレコードを取得して表示するViewの場合はどのようになるのでしょうか。
まずは、urls.py
の設定から確認してみましょう。
今回もpath
関数の定義から。
path('<int:pk>/', ArticleDetailView.as_view(), name='detail'),
このようにしました。
name='detail'
を追加しています。
先ほどのListViewのpath
関数とは違い、<int:pk>
のように、URLに数値などの情報が入ります。
このような場合、どのように{% url %}
構文を記述するか説明します。
例えばhttps://foo.com/1/
のように、<int:pk>
部分に1
が入る場合のurl
構文は以下の通りです。
{% url 'detail' 1 %}
name='detail'
と定義されていますので、最初の引数にはdetail
が入ります。ここは単純な文字列ですのでPython同様クォートしてください。
第二引数が今回の肝で、いまは数値の1が入っています。数値ですのでクォートしていません。<int:pk>
という定義ですので、数値です。
このように定義すると、生成されるURLは以下のようになります。
/1/
つまり、https://foo.com/1/
のようなURLとなります。
なんとなく分かると思いますが、第二引数に渡された数値が<int:pk>
部分に適用されています。
もう一例出したいので、path
関数を書き換えてみましょう。
path('detail/<int:pk>/', ArticleDetailView.as_view(), name='detail'),
第一引数にdetail
を追加してみました。
では、URLを出力してみましょう。
{% url 'detail' 1 %}
前回と同じです。この結果は以下の通りです。
/detail/1/
つまり、https://foo.com/detail/1/
のようなURLを生成します。
path
関数の内容と見比べてみてください。<int:pk>
部分が数値で置き換えられたようなURLになっていると思います。
もちろん、これでは不便ですのでテンプレートでは引数に変数を使用します。
{% url 'detail' object.id %}
このような感じです。
テンプレートに記述する際は数値をハードコードすることはほとんどないと思います。
変数を利用して生成することがほとんどです。
urls.pyを分割する
プロジェクト開発を進めていくとたくさんのアプリケーションを定義していくことになります。
アプリケーションが増えていくことこそDjangoプロジェクトの成長だからです。
しかし、そうなるとURLの定義がとても大変になります。
様々なアプリケーションのURLをすべてひとつのファイルに記述していくととても長くなってしまい、見通しも悪くなります。
そういったことを防止するために、Djangoではurls.py
を分割することができます。
本項では分割について解説します。
分割のメリット
分割のメリットは2つあります。
ひとつめは、前述の通りURL定義の見通しが良くなることです。
このメリットは言わずもがなで、できる限り複雑さを排除すること重要です。
もうひとつは、ネームスペースを作成することができることです。
ネームスペースがあることによって、app1とapp2でname='detail'
のような同じ名前のpath
関数を定義できるようになります。
include関数
urls.py
を分割するにあたり、はじめに知っておかなければいけない関数があります。
それが、include
関数です。
この関数は文字どおり他のurls.py
を読み込む事ができます。
読み込む際にネームスペースを作成することができる、というものです。
include 関数は他のurls.py(に相当するファイル)を読み込むことができる
使い方は以下のとおりです。
path('articles/', include("bar.article_urls", namespace='article')),
今までViewを設定していたところにinclude
関数を記述します。
今回定義した内容は以下のとおりです。
path('設定するURL', include(読み込むurls.py, namespace='ネームスペースの名称')),
では解説していきます。
path
関数の第一引数であるURLは同じです。このURLにアクセスされた際に動作します。
第二引数のinclude
関数が今回の本丸ですね。
include
関数の第一引数は文字列で、読み込むurls.py(に相当するファイル)
を指定します。相当するファイル、と記載しているのはファイル名はurls.py
である必要はないからです。わかりやすい名前で良いと思います。今回はarticle_urls.py
としています。
指定の仕方はPythonのモジュール指定と同じです。ファイルの改装をドットで表現し、拡張子.py
は必要ありません。
名前付き引数に指定してあるnamespace
はその名のとおりネームスペースの名前を設定します。今回はarticle
としています。
app_nameをincludeされる側に記述する
実はinclude
関数でネームスペースを設定するだけではうまくいきません。
include
される側にも設定が必要です。
それが、app_name
です。
app_name
は難しくなく、ネームスペースの名前を代入します。
例えば、今回の例は以下のとおりです。
app_name = 'article'
このように、include
関数で定義したnamespace='article'
とapp_name='article'
の'article'
が同じであるのは偶然ではありません。
ネームスペースの指定ですので、同じ名称を設定してください。
実際に分割してみる
さて、長々と説明してきましたが、実際に分割してみます。
元になるurls.py
は以下のとおりです。
from django.contrib import admin
from django.urls import path
from bar.views import ArticleListView, sample, ArticleDetailView
urlpatterns = [
path('admin/', admin.site.urls),
]
urlpatterns += [
path('', ArticleListView.as_view(), name='index'),
path('<int:pk>/', ArticleDetailView.as_view(), name='detail'),
path('sample-url/', sample),
]
ここまでの記事で出てきた内容です。
現在のファイルツリーは以下のとおりです。
一部作業に必要の無いファイルは意図的に表示していません。
カッコの中は説明を追加したものです。
├── bar (アプリケーション) │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── tests.py │ └── views.py ├── foo (プロジェクト) │ ├── init.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py (分割するファイル) │ └── wsgi.py └── manage.py
まずは、bar
アプリケーションにarticle_urls.py
を作成します。
一から作成するのが面倒な方はfoo
プロジェクトのurls.py
をコピーして、不要なコードを削除しても良いと思います。私はコピーして使っています。
次に、foo
プロジェクトのurls.py
とbar
アプリケーションのarticle_urls.py
を更新します。
まずは、元々あるurls.py
は下記の通りです。
from django.contrib import admin
from django.urls import path, include # include 追加
from bar.views import ArticleListView, sample, ArticleDetailView
urlpatterns = [
path('admin/', admin.site.urls),
]
urlpatterns += [
# 下の行を追加
path('articles/', include("bar.article_urls", namespace='article')),
path('sample-url/', sample),
]
コメントで示していますが、include
関数をインポートしています。
さらに、urlpatterns
リストの内部にinclude
関数を使ってarticle_urls.py
を読み込んでいます。
ネームスペースはarticle
としています。
続いて、先ほど新規作成したarticle_urls.py
は下記の通りです。
from django.urls import path
from bar.views import ArticleListView, ArticleDetailView
app_name = 'article'
urlpatterns = [
path('', ArticleListView.as_view(), name='index'),
path('<int:pk>/', ArticleDetailView.as_view(), name='detail'),
]
新たに作ったarticle_urls.py
にはクラスベースビューを使ったpath
関数を設定しています。
現在のファイルツリーは以下の通りです。
├── bar (アプリケーション) │ ├── admin.py │ ├── apps.py │ ├── models.py │ ├── tests.py │ ├── article_urls.py (新規追加したファイル) │ └── views.py ├── foo (プロジェクト) │ ├── init.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py (分割するファイル) │ └── wsgi.py └── manage.py
このようになっています。
反応するURLの違い
ここで注意点がひとつあります。
urls.py
では連携させるURLを設定していました。
2つのurls.py
では連携するURLが少し違います。例えばhttps://foo.com/article/1/
で説明します。
https://foo.com /article/ 1/
元のurls.py
が連携している部分は /article/
部分です。
新規作成したarticle_urls.py
が連携している部分 は 1/
部分です。
図で示すと以下のとおりです。相変わらず下手な図ですみません。
urls.py
とarticle_urls.py
がURLを処理する際、どの部分を担当しているかご理解いただけたと思います。
urls.py
を分割することでネームスペースの作成と、include
したファイルに処理を委譲することができます。
ネームスペースのある{% url %}
include
を使用するとネームスペースが作成されることがわかりました。
この状態でテンプレートに以下の記述をしてみましょう。
{% url 'detail' 1 %}
のように記述してもエラーとなります。
理由はネームスペースの記述がないからです。
では、そのネームスペースを含む {% url %}
はどのように記述するのでしょうか。
答えは割と簡単で、以下のようにします。
{% url 'article:detail` 1 %}
これを自然言語で表すと以下のようになります。
{% urlinclude
関数で定義したネームスペース:path
関数で定義したname
引数 %}
name
を定義する前に、名字のようにネームスペースを定義して、名字と名前をコロンで区切る、というわけです。
落ち穂拾い
分割するURLは必ずしもurls.py
というファイル名でなくても問題ありません。
では、最初から用意されているurls.py
はファイル名を変更しても良いのでしょうか?
答えは「良い」です。
しかし、設定が必要になります。
設定ファイルsettings.py
に以下の項目があります。
ROOT_URLCONF = 'foo.urls'
ここで、foo
フォルダ内のurls.py
が指定されています。
ここを変更すればurls.py
のファイル名を変更することができます。
しかし、デフォルトでurls.py
となっていますので特別な理由がない限り変更する必要はないと思います。
まとめ
ウェブアプリケーションフレームワークであるDjangoにとってURLは大変重要な要素です。
URLに対してViewがページを生成し、それを訪問者が閲覧します。それが結果です。
大きいプロジェクトほどURLの管理、Viewの管理が大変になります。
分割を上手に使って効率よく管理してください。