Open1

【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である。

django/todo/views.py
def hello(request):
    return HttpResponse("Hello Django")

文字列を返すだけ(URLから引数を受け取る)

views.pyに以下を追加する。引数を受け取り文字列として出力させる。

django/todo/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型などがある。

django/todo/urls.py
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、第二引数にテンプレートファイル名を渡すとテンプレートを表示できる。さらに第三引数にコンテキストという辞書(キーと値のペア)を渡すと、テンプレートに変数を入れられる。

django/todo/views.py
# テンプレートを表示する
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に追加。

django/todo/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()から受け取った変数を表示することができる。

django/todo/templates/todo/template_test.html
<!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にデータを表示させる。
django/todo/views.py
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' %})。
django/todo/urls.py
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>内に表示する。
django/todo/templates/todo/todo_list.html
<!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に値を渡すことができる。
django/todo/views.py
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

django/todo/urls.py
urlpatterns = [
    ⋮
    path("detail/<int:pk>/", views.todo_detail_view, name="detail"),
]

テンプレート

templates/todoディレクトリにtodo_detail.htmlファイルをつくり、以下を入力する。

  • 1件だけなので、一覧画面のようにforループを回す必要はない。
  • 一覧画面に戻るにはhistory.back()を使用した。ブラウザで前のページに戻るメソッドであり、Djangoは関係ない。
django/todo/templates/todo/todo_detail.html
<!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.methodPOSTGETで分岐している。POSTGETとは何か、そもそもrequestmethodは何かという話だが、簡単にまとめてみた(参考)。

  • request
    • Webページにアクセスしたときに、Webサーバに伝えられる要求
  • method
    • サーバへの要求の種類
    • 主にPOST、GETがある。他にはPUT、DELETEなど。
  • POST
    • クライアントからのデータを受信するよう要求(入力フォームの値とか)
  • GET
    • データをクライアントに送信するよう要求(画面を表示とか)

POSTは入力フォームの値を保存するとき、GETは画面を表示するときなどに使われるということが分かった。今回のコードでそれらのメソッドがいつ送信されるかというと、以下の場合である。

  • POST
    • 新規作成画面にて、新規作成ボタンを押す。
  • GET
    • 一覧画面にて、新規作成へのリンク<a href="{% url 'todo:create' %}">新規作成</a>を押す。
    • アドレスバーに新規作成のURLhttp://127.0.0.1:8000/todo/create/を直接打ち込む。

今回のコードでmethodごとにどのような処理が行われるかというと、

  • POST
    • 新規作成フォームから送信した値がrequest.POSTに辞書として入っており、request.POST["text"]のようにキーを指定して値を取得する。
    • 値をcreate()メソッドで保存する。
    • 保存したらredirect()で一覧画面にリダイレクトする。redirect()の引数にビューやパス、URLを指定すると、そちらに遷移する。
  • GET
    • render()で新規作成画面を表示する。
django/todo/views.py
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

django/todo/urls.py
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>を使うときはこれを記入しなければならない。
django/todo/templates/todo/todo_form.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:create' %}">
      {% csrf_token %}
      <input type="text" name="text" />
      <input type="submit" value="新規作成" />
    </form>
  </body>
</html>

関数ベース|更新

ビュー

更新画面も新規作成画面と同じく、POSTGETで処理が分かれる。

  • まず選択したレコードをTodo.objects.get(pk=pk)で取得する。
  • POSTのとき
    • 入力フォームの値をrequest.POST["text"]で取得する。
    • レコードをsave()メソッドで更新する。
    • 一覧画面にリダイレクトする。
  • GETのとき
    • 選択したレコードを埋め込み、更新画面を表示する。

新規作成との比較

  • POSTなら入力フォームの値を取得し保存する、GETなら入力フォームを表示するという流れは新規作成画面と同じ。
  • ただし更新画面は既存の値を変更するので、まず選択したレコードを取得するという違いがある。
django/todo/views.py
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

django/todo/urls.py
urlpatterns = [
    ⋮
    path("update/<int:pk>/", views.todo_update_view, name="update"),
]

テンプレート

templates/todoディレクトリにtodo_update.htmlファイルをつくり、以下を入力する。

django/todo/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>

関数ベース|削除

ビュー

削除画面も新規作成画面や更新画面と同じく、POSTGETで処理が分かれる。

  • まずTodo.objects.get(pk=pk)で選択したレコードを取得する。
  • POSTのとき
    • delete()メソッドで選択したレコードを削除する。
    • 一覧画面にリダイレクトする。
  • GETのとき
    • 選択したレコードを埋め込み、削除画面を表示する。

他との比較

  • まず選択したレコードを取得し、POSTなら処理、GETなら画面表示という流れが更新画面とほぼ同じ。
django/todo/views.py
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

django/todo/urls.py
app_name = "todo"
urlpatterns = [
    ⋮
    path("delete/<int:pk>/", views.todo_delete_view, name="delete"),
]

テンプレート

templates/todoディレクトリにtodo_confirm_delete.htmlファイルをつくり、以下を入力する。

django/todo/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つの間の違いを見てみた(後編)

参考