【Django】関数ベースビューとクラスベースビューでそれぞれTodoリストをつくり、2つの間の違いを見てみた(前編)
はじめに
長いので前編・後編に分割した。
- 前編:Django初期設定・関数ベースビュー
- 後編:クラスベースビュー
後編はこちら
経緯
Djangoには関数ベースビューとクラスベースビューという2つのビューがある。関数ベースよりクラスベースの方が簡潔なので、チュートリアルや解説サイトでは関数ベースビューの練習はすぐ終わり、クラスベースビューの練習に多くの時間を割いているように思う。私も関数ベースビューの練習はそこそこにして、これまでクラスベースビューの練習に重点を置いてきた。しかしDjangoを使い始めて1年以上経ったが、恥ずかしながら未だにピンときていない。
それはなぜかというと、クラスベースビューの処理の多くはショートカットされているがゆえに、処理の流れを追いづらいためではないかと思う。関数ベースビューでは必要な処理をひとつひとつ書いていくので、しりとりのように追いかけることができる。しかしクラスベースビューではお決まりの処理の多くが省略される。実業務ではクラスベースビューの方が良いとは思うが、最初のうちは関数ベースビューで処理の流れを体得した方が良いのではないかと考えた。
そこでまず関数ベースビューで簡単なTodoリストをつくる。Todoリストをつくるのはデータベースの基本操作であるCRUD(Create、Read、Update、Delete)を網羅できるからである。その後同じものをクラスベースビューで作成する。同じことを行う2つのビューがどう異なるのか調べて、ビューへの理解を深める。
- 対象者
- Djangoを一通り触って流れは把握しているが、なんとなくしっくりきてない人(つまり私)。
- どんなTodoリスト?
- テキストだけ保存できる。
- CSSは使わない。練習なので凝ったものはつくらない。
Todoリストのサンプル
TODO:ここにサンプル
一覧画面
一覧画面にしてホーム画面。各Todoを閲覧できるとともに、新規作成画面、詳細画面、更新画面、削除画面へと遷移できる。
詳細画面
一覧で選択したTodoのpk(プライマリーキー)、text、作成日時、更新日時を表示する。
新規作成画面
Todoを新しく作成する。作成したら一覧画面に戻る。
更新画面
一覧で選択したTodoを更新する。更新したら一覧画面に戻る。
削除画面
一覧で選択したTodoを削除してよいか確認する。削除したら一覧画面に戻る。
Djangoの初期設定
プロジェクトディレクトリの作成
任意のディレクトリにプロジェクトディレクトリをつくる。
> mkdir django
> cd django
仮想環境の作成(任意)
仮想環境をつくる。(仮想環境名)には任意の名前を入力する。ここではvenv
。
> python -m venv 仮想環境名
仮想環境を有効化する。有効化されるとコマンドラインの先頭に環境名が表示される。
> venv/Scripts/activate
(venv)>
Djangoのインストール
(venv)> pip install django
プロジェクト作成
Djangoプロジェクトを作成する。(プロジェクト名)には任意の名前を入力する。ここではconfig
。
(venv)> django-admin startproject プロジェクト名 .
起動確認
開発用サーバーを起動する。
(venv)> python manage.py runserver
http://localhost:8000/
にアクセスする。ロケット打ち上げのページが表示されたら、プロジェクトの起動確認は完了。
アプリケーション作成
アプリケーションを作成する。(アプリ名)には任意の名前を入力する。ここではtodo
。
(venv)> python manage.py startapp アプリ名
設定変更
django/config/settings.py
を編集する。
先ほど作成したアプリケーションを登録する。
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
+ "todo",
]
言語設定を変更する。
- LANGUAGE_CODE = 'en-us'
+ LANGUAGE_CODE = 'ja'
タイムゾーンを変更する。
- TIME_ZONE = 'UTC'
+ TIME_ZONE = 'Asia/Tokyo'
Model を作成する
django/todo/models.py
を編集する。
from django.db import models
class Todo(models.Model):
text = models.CharField(max_length=64)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
フィールドは以下の3つ。
-
text
- Todoリストのテキスト
-
created_at
- 作成日時
-
auto_now_add=True
にするとオブジェクトを作成したとき、自動で現在日時をセットしてくれる(参考:Django Document)
-
updated_at
- 更新日時
-
auto_now=True
にするとオブジェクトを更新したとき、自動で現在日時をセットしてくれる
マイグレーションする
(venv)> python manage.py makemigrations
(venv)> python manage.py migrate
Viewを作成する
django/todo/views.py
に以下を入力する。まずはお試しとして簡単な関数ベースビューだけ。
from django.http import HttpResponse
from django.shortcuts import render
def hello(request):
return HttpResponse("Hello Django")
URLを設定する
django/config/urls.py
を編集する。
from django.contrib import admin
- from django.urls import path
+ from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
+ path("todo/", include("todo.urls")),
]
django/todo
ディレクトリにurls.py
ファイルを作成し、以下を入力する。
from django.urls import path
from . import views
urlpatterns = [
path("hello/", views.hello),
]
http://localhost:8000/todo/
にアクセスすると、Hello Django
と表示されるはず。とりあえず初期設定は終了。
VSCode 拡張機能(任意)
あると便利。VSCodeの拡張機能欄からそれぞれインストールする。
Black Formatter
スペースや改行を整形してくれる。
isort
インポートの順序を規約に従い整列してくれる。
indent-rainbow
インデントを色分けして見やすくしてくれる。
簡単な関数ベースビュー
これから関数ベースビューの練習に入る。だが関数ベースとかクラスベースとか以前に、そもそもビューが何なのかピンときてない気がする。たいていビューは難しそうな処理を行うが、その本質は「リクエストを受けてレスポンスを返す」ことである。CRUDの前に、まずは簡単なビューをつくってイメージをつかもうと思う。
文字列を返すだけ
初期設定で作成したhello()
は、リクエストを受けてレスポンスとして文字列Hello Django
を返すだけの最も簡単なviewである。
def hello(request):
return HttpResponse("Hello Django")
文字列を返すだけ(URLから引数を受け取る)
views.py
に以下を追加する。引数を受け取り文字列として出力させる。
# 文字列に変数を入れる1
def hello_str(request, str):
return HttpResponse(f"Hello {str}")
# 文字列に変数を入れる2
def hello_slug(request, slug):
return HttpResponse(f"Hello {slug}")
urls.py
にも追加する。URLの<str:str>
や<slug:slug>
を通して、関数に文字列を渡すことができる。<型:引数名>
でありstr
型の場合は「スラッシュを除く文字列」、slug
型の場合は「半角小大英数字とハイフンとアンダースコア」を示す。他にもint
型やuuid
型などがある。
urlpatterns = [
path("hello/", views.hello),
path("hello_str/<str:str>/", views.hello_str),
path("hello_slug/<slug:slug>/", views.hello_slug),
]
hello_str()
を試す。ブラウザのアドレスバーに以下のURLを入力する。
str
型は改行できたり全角文字を入れられたりと、何でもありな印象。
http://127.0.0.1:8000/todo/hello_str/Python/
Hello Python
http://127.0.0.1:8000/todo/hello_str/くぁwせdrftgyふじこlp/
Hello くぁwせdrftgyふじこlp
http://127.0.0.1:8000/todo/hello_str/は?夜だけど/
Hello は?夜だけど
http://127.0.0.1:8000/todo/hello_str/aaa<br>bbbbb/
Hello aaa
bbbbb
http://127.0.0.1:8000/todo/hello_str/😎😎😎/
Hello 😎😎😎
hello_slug()
を試す。slug
型は変なのを入れられないようだ。
http://127.0.0.1:8000/todo/hello_slug/Python/
Hello Python
http://127.0.0.1:8000/todo/hello_slug/くぁwせdrftgyふじこlp/
Page not found
http://127.0.0.1:8000/todo/hello_slug/は?夜だけど/
Page not found
http://127.0.0.1:8000/todo/hello_slug/aaa<br>bbbbb/
Page not found
http://127.0.0.1:8000/todo/hello_slug/😎😎😎/
Page not found
テンプレートを返すだけ
テンプレートを表示することもできる。
views.py
に以下を追加する。render()
はテンプレートを表示する関数である。第一引数にrequest
、第二引数にテンプレートファイル名を渡すとテンプレートを表示できる。さらに第三引数にコンテキストという辞書(キーと値のペア)を渡すと、テンプレートに変数を入れられる。
# テンプレートを表示する
def template(request):
return render(request, "todo/template_test.html")
# テンプレートに変数を入れる
def template_str(request, str):
context = {"str": str}
return render(request, "todo/template_test.html", context)
urls.py
に追加。
urlpatterns = [
path("hello/", views.hello),
path("hello_str/<str:str>/", views.hello_str),
path("hello_slug/<slug:slug>/", views.hello_slug),
+ path("template/", views.template),
+ path("template_str/<str:str>/", views.template_str),
]
todo
ディレクトリ内にtemplates
ディレクトリ、templates
ディレクトリ内にtodo
ディレクトリ、todo
ディレクトリ内にtemplate_test.html
ファイルをつくる。
django
└── todo
├── views.pyなど
└── templates
└── todo
└── template_test.html
template_test.html
に以下を入力する。テンプレートに{{ 変数名 }}
を入力すると、render()
から受け取った変数を表示することができる。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
</head>
<body>
<h1>Hello Template {{ str }}</h1>
</body>
</html>
ブラウザのアドレスバーに以下のURLを入力すると、文字列がでてくるはず。
http://127.0.0.1:8000/todo/template/
Hello Template
http://127.0.0.1:8000/todo/template_str/!!!!!!/
Hello Template !!!!!!
http://127.0.0.1:8000/todo/template_str/うわあああああああ!!!!!!/
Hello Template うわあああああああ!!!!!!
リクエストしたら何かレスポンスが返ってくる、その練習を行った。それではTodoリストに入る。
関数ベース|一覧
ビュー
views.py
に以下を追加する。先ほどまでに書いたimportや関数などは省略し、関係あるところだけ示す(以下同じ)。
-
Todo.objects.all()
でデータを全て取得する。 - その値を
object_list
というキーでcontext
に格納する。 -
render()
にcontext
を渡し、todo/todo_list.html
にデータを表示させる。
from django.http import HttpResponse
from django.shortcuts import render
from todo.models import Todo
⋮
def todo_list_view(request):
object_list = Todo.objects.all()
context = {"object_list": object_list}
return render(request, "todo/todo_list.html", context)
URL
urls.py
に以下を追加する。
-
path()
に引数name
を追加した。これをテンプレート内の{% url 'name' %}
のname
部分に書けば、ビューにアクセスすることができる(例:{% url 'list' %}
)。 -
app_name
も追加した。これを{% url 'app_name:name' %}
のように書けば、アプリ間でname
がかぶってもビューにアクセスできる(例:{% url 'todo:list' %}
)。
app_name = "todo"
urlpatterns = [
⋮
path("list/", views.todo_list_view, name="list"),
]
テンプレート
templates/todo
ディレクトリにtodo_list.html
ファイルをつくり、以下を入力する。
-
object_list
はビューから渡されたアレである。 -
{% for object in object_list %}...{% endfor %}
はDjangoのテンプレートで使われるforループである。object_list
がリストなので、ループして1個ずつ取り出し、<ul>
内に表示する。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
</head>
<body>
<a href="{% url 'todo:create' %}">新規作成</a>
<ul>
{% for object in object_list %}
<li>
<button onclick="location.href=`{% url 'todo:detail' object.pk %}`">
詳細
</button>
<button onclick="location.href=`{% url 'todo:update' object.pk %}`">
更新
</button>
<button onclick="location.href=`{% url 'todo:delete' object.pk %}`">
削除
</button>
{{ object.pk }}. {{ object.text }}
</li>
{% endfor %}
</ul>
</body>
</html>
関数ベース|詳細
ビュー
- データを取得し、コンテキストに格納し、
render()
に渡してテンプレートを表示するという流れは一覧画面と同じ。 - ただ、一覧画面は
Todo.objects.all()
で全てのデータを取得する。こちらはTodo.objects.get(pk=pk)
で選択したレコードを1つだけ取得している。 - pkは一覧画面の詳細ボタン「onclick="location.href=
{% url 'todo:detail' object.pk %}
"」から取得している。{% url '...' 値 %}
と書くと、URLに値を渡すことができる。
from django.http import HttpResponse
from django.shortcuts import render
from todo.models import Todo
⋮
def todo_detail_view(request, pk):
object = Todo.objects.get(pk=pk)
context = {"object": object}
return render(request, "todo/todo_detail.html", context)
URL
urlpatterns = [
⋮
path("detail/<int:pk>/", views.todo_detail_view, name="detail"),
]
テンプレート
templates/todo
ディレクトリにtodo_detail.html
ファイルをつくり、以下を入力する。
- 1件だけなので、一覧画面のようにforループを回す必要はない。
- 一覧画面に戻るには
history.back()
を使用した。ブラウザで前のページに戻るメソッドであり、Djangoは関係ない。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
</head>
<body>
<a href="#" onclick="history.back()">戻る</a>
<div>pk: {{ object.pk }}</div>
<div>text: {{ object.text }}</div>
<div>作成日時: {{ object.created_at }}</div>
<div>更新日時: {{ object.updated_at }}</div>
</body>
</html>
関数ベース|新規作成
ビュー
新規作成画面は一覧画面や詳細画面と違い、request.method
がPOST
・GET
で分岐している。POST
・GET
とは何か、そもそもrequest
やmethod
は何かという話だが、簡単にまとめてみた(参考)。
- request
- Webページにアクセスしたときに、Webサーバに伝えられる要求
- method
- サーバへの要求の種類
- 主にPOST、GETがある。他にはPUT、DELETEなど。
- POST
- クライアントからのデータを受信するよう要求(入力フォームの値とか)
- GET
- データをクライアントに送信するよう要求(画面を表示とか)
POST
は入力フォームの値を保存するとき、GET
は画面を表示するときなどに使われるということが分かった。今回のコードでそれらのメソッドがいつ送信されるかというと、以下の場合である。
-
POST
- 新規作成画面にて、新規作成ボタンを押す。
-
GET
- 一覧画面にて、新規作成へのリンク
<a href="{% url 'todo:create' %}">新規作成</a>
を押す。 - アドレスバーに新規作成のURL
http://127.0.0.1:8000/todo/create/
を直接打ち込む。
- 一覧画面にて、新規作成へのリンク
今回のコードでmethod
ごとにどのような処理が行われるかというと、
-
POST
- 新規作成フォームから送信した値が
request.POST
に辞書として入っており、request.POST["text"]
のようにキーを指定して値を取得する。 - 値を
create()
メソッドで保存する。 - 保存したら
redirect()
で一覧画面にリダイレクトする。redirect()
の引数にビューやパス、URLを指定すると、そちらに遷移する。
- 新規作成フォームから送信した値が
-
GET
-
render()
で新規作成画面を表示する。
-
from django.http import HttpResponse
from django.shortcuts import redirect, render
from todo.models import Todo
⋮
def todo_create_view(request):
if request.method == "POST":
text = request.POST["text"]
Todo.objects.create(text=text)
return redirect("todo:list")
if request.method == "GET":
return render(request, "todo/todo_form.html")
URL
urlpatterns = [
⋮
path("create/", views.todo_create_view, name="create"),
]
テンプレート
templates/todo
ディレクトリにtodo_form.html
ファイルをつくり、以下を入力する。
-
<form>
は入力フォームをつくるhtml要素。<form>...</form>
の中に<input>
タグなどのフォーム部品を配置してフォームをつくる。type = "submit"
を設定した送信ボタンを押したとき、入力した値をmethod
属性に指定したメソッドで、action
属性に指定したURLへ送信する。 -
{% csrf_token %}
はクロスサイトリクエストフォージェリ対策。詳しいことは省略するが、Djangoで<form>
を使うときはこれを記入しなければならない。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
</head>
<body>
<a href="#" onclick="history.back()">戻る</a>
<form method="POST" action="{% url 'todo:create' %}">
{% csrf_token %}
<input type="text" name="text" />
<input type="submit" value="新規作成" />
</form>
</body>
</html>
関数ベース|更新
ビュー
更新画面も新規作成画面と同じく、POST
かGET
で処理が分かれる。
- まず選択したレコードを
Todo.objects.get(pk=pk)
で取得する。 -
POST
のとき- 入力フォームの値を
request.POST["text"]
で取得する。 - レコードを
save()
メソッドで更新する。 - 一覧画面にリダイレクトする。
- 入力フォームの値を
-
GET
のとき- 選択したレコードを埋め込み、更新画面を表示する。
新規作成との比較
-
POST
なら入力フォームの値を取得し保存する、GET
なら入力フォームを表示するという流れは新規作成画面と同じ。 - ただし更新画面は既存の値を変更するので、まず選択したレコードを取得するという違いがある。
from django.http import HttpResponse
from django.shortcuts import redirect, render
from todo.models import Todo
⋮
def todo_update_view(request, pk):
object = Todo.objects.get(pk=pk)
if request.method == "POST":
object.text = request.POST["text"]
object.save()
return redirect("todo:list")
else:
context = {"object": object}
return render(request, "todo/todo_update.html", context)
URL
urlpatterns = [
⋮
path("update/<int:pk>/", views.todo_update_view, name="update"),
]
テンプレート
templates/todo
ディレクトリにtodo_update.html
ファイルをつくり、以下を入力する。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
</head>
<body>
<a href="#" onclick="history.back()">戻る</a>
<form method="POST" action="{% url 'todo:update' object.pk %}">
{% csrf_token %}
<input type="text" name="text" value="{{ object.text }}" />
<input type="submit" value="更新" />
</form>
</body>
</html>
関数ベース|削除
ビュー
削除画面も新規作成画面や更新画面と同じく、POST
かGET
で処理が分かれる。
- まず
Todo.objects.get(pk=pk)
で選択したレコードを取得する。 -
POST
のとき-
delete()
メソッドで選択したレコードを削除する。 - 一覧画面にリダイレクトする。
-
-
GET
のとき- 選択したレコードを埋め込み、削除画面を表示する。
他との比較
- まず選択したレコードを取得し、
POST
なら処理、GET
なら画面表示という流れが更新画面とほぼ同じ。
from django.http import HttpResponse
from django.shortcuts import redirect, render
from todo.models import Todo
⋮
def todo_delete_view(request, pk):
object = Todo.objects.get(pk=pk)
if request.method == "POST":
object.delete()
return redirect("todo:list")
else:
context = {"object": object}
return render(request, "todo/todo_confirm_delete.html", context)
URL
app_name = "todo"
urlpatterns = [
⋮
path("delete/<int:pk>/", views.todo_delete_view, name="delete"),
]
テンプレート
templates/todo
ディレクトリにtodo_confirm_delete.html
ファイルをつくり、以下を入力する。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
</head>
<body>
<a href="#" onclick="history.back()">戻る</a>
<form method="POST">
{% csrf_token %}
<div>pk: {{ object.pk }}</div>
<div>text: {{ object.text }}</div>
<div>削除してもよろしいですか?</div>
<input type="submit" value="削除" />
</form>
</body>
</html>
ここまでファイルを作成したら、http://127.0.0.1:8000/todo/
にアクセスする。まだ何のデータも作成していないので真っ白な画面が表示されるはず。上部の「新規作成」リンクを押してTodoを作成すると、一覧画面に戻りTodoが表示される。そこから詳細、更新、削除も行えるので試してみてほしい。
後編へつづく
クラスベースビューはこちら。
【Django】関数ベースビューとクラスベースビューでそれぞれTodoリストをつくり、2つの間の違いを見てみた(後編)