haku-maiのブログ

インフラエンジニアですが、アプリも作ります。

【Django3.1】generic viewを使わずにfunctionを利用してCRUDを行う方法

本記事で行うこと

※なお通常Djangoでアプリケーションを作成する際は、generic viewを使い、機能を拡張したい時はfunctionをオーバーライドすることが自分は多い。
余談だが、generic viewが持っているfunctionは以下を参照すると良い。
Django Class-Based-View Inspector -- Classy CBV

なお本記事のソースコードgithub上に公開しています。
GitHub - n-guitar/django-samples at sample.function-view

モチベーション

  • Djangoを教える時のサンプルとして利用したいため。

環境

$ sw_vers
ProductName:    macOS
ProductVersion: 11.1
BuildVersion:   20C69
$ python -V
Python 3.8.0

事前準備 pyenv環境

$ python -m venv env
$ source env/bin/activate
$ export PATH=$PWD/env/bin:$PATH

Djangoのインストール

$ pip install django==3.1.3

Django 初期設定

  • プロジェクトとアプリを作成
  • プロジェクトでtempletsフォルダを設定
$ django-admin startproject confug .
$ django-admin startapp samplecrud
$ mkdir templates
  • settings.py編集 appの追加とtemplateフォルダを参照するように編集

confug/settings.py

・・・
# samplecrudを追加
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'samplecrud', # 追加
]
・・・
# TEMPLATESのDIRSを変更
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ['templates'], # 変更
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

動作確認

$ python manage.py runserver

http://127.0.0.1:8000/ にアクセスして以下が表示されればOK f:id:n-guitar:20201228201112p:plain:w600
以上で準備完了。
一旦control + cで止めておく。

現時点で以下のようなディレクトリ構成になっている。
f:id:n-guitar:20201228201643p:plain:w300

CRUD用のDBを作成

今回は以下のようなDBを作成する

Column Field
title CharField(max_length=100)
memo TextField(max_length=255)

samplecrud/models.py

from django.db import models

class Todo(models.Model):
    title = models.CharField(max_length=100)
    memo = models.TextField(max_length=255)

    def __str__(self):
            return self.title

DBの反映

$ python manage.py makemigrations samplecrud
Migrations for 'samplecrud':
  samplecrud/migrations/0001_initial.py
    - Create model Todo

$ python manage.py migrate samplecrud 
Operations to perform:
  Apply all migrations: samplecrud
Running migrations:
  Applying samplecrud.0001_initial... OK

Read画面の作成

早速Read画面を作成するがその前に、URLのつなぎこみを行う。 ※直接configからURLを指定してもいいが、通常アプリごとに作成する多い。

URLのつなぎこみ

$ touch samplecrud/urls.py

現時点のディレクトリ構造は以下の通りになっている。 f:id:n-guitar:20201228202723p:plain:w300

URLのつなぎこみ。
confug/urls.pyを編集する

from django.contrib import admin
from django.urls import path, include # include追加

urlpatterns = [
    path('admin/', admin.site.urls),
    path('samplecrud/', include('samplecrud.urls')), # 追加
]

上記のURLは『http://127.0.0.1:8000/samplecrud/』でアクセスした時、『samplecrud/urls.py』記載されているURLにルーティングするという意味。

samplecrud/urls.pyを編集する

from django.urls import path
from .views import list_page

urlpatterns = [
    path('list/', list_page, name='list'),
]

Read画面の作成

いよいよ画面を作成していく。

samplecrud/views.py

from django.shortcuts import render
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

object_list = Todo.objects.all()
はToDo Tableをすべて(all)取得し、object_listに格納している。
DjangoではSQLではなくORMを使いdatabaseを操作することが多い。
(SQLを利用することも可能だが、SQLインジェクション対策を考えなくてはいけなくなるので、特別な理由がなければORMを使うほうが良い。)
※ORMの詳細はマニュアルを参照
Making queries | Django documentation | Django

return render(request, 'list.html', {'object_list':object_list})
はlist.htmlに先程格納したobject_listを辞書型で渡し、レンダリングしている。

レンダリング先のhtmlがないので作成する。
templates/list.htmlの作成

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>{{ item.title }}: {{ item.memo }}</li>
    {% endfor %}
</ul>

Todo tableから取得した値がobject_listというオブジェクトに格納され、list.htmlに渡される

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ にアクセスする

以下のように表示されればOK。
今はテーブルに何もレコードがないため、何も表示されない。 f:id:n-guitar:20201228210609p:plain:w300

Create画面の作成

続けてDBのレコードを追加する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import list_page, create_page # 追記

urlpatterns = [
    path('list/', list_page, name='list'),
    path('create/', create_page, name='create'), # 追記
]

こちらは先程のlistと同じ

samplecrud/views.py

from django.shortcuts import render, redirect
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

def create_page(request):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.create(title=title, memo=memo)
        return redirect('list')
    return render(request, 'create.html')

createのときはどのGETとPOSTメソッドを処理する必要がある。
GETの時はCreate画面を表示し、POSTの時はformに入力された値を受け取ってdatabaseを更新させる必要がある。
Todo.objects.create(title=title, memo=memo) は受け取ったパラメータでToDo tableにレコード追加している。

※通常databaseを更新する時はバリデーションをすべきであるが本記事ではわかりにくくなるので行わないでおく。

templates/create.html

<h1>Create Page</h1>

<form action="" method="POST"> {% csrf_token %}
    <p>
        <label for="id_title">Title:</label>
        <input type="text" name="title" maxlength="100" required="" id="id_title">
    </p>
    <p>
        <label for="id_memo">Memo:</label>
        <textarea name="memo" cols="40" rows="10" maxlength="255" required="" id="id_memo"></textarea>
    </p>
    <input type="submit" value="作成する">
</form>

</form>

inputタグにname="※POSTで受け取りたい変数名"を入れることでviews.pyでその変数名で受け取ることができる。
Djangoは『 {% csrf_token %}』をつけることでデフォルトでCSRFの検証を行ってくれる。(逆につけないとエラーになる)

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/create/ にアクセスすると以下によう表示され、更新するとlistが表示されて先程更新したデータが表示される。
何件か登録してみるとすべて表示されることがわかる。

f:id:n-guitar:20201228212434p:plain:w300
f:id:n-guitar:20201228212920p:plain:w300

Update画面の作成

続けてDBのレコードを更新する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import list_page, create_page, update_page

urlpatterns = [
    path('list/', list_page, name='list'),
    path('create/', create_page, name='create'),
    path('update/<int:pk>', update_page, name='update'),
]

こちらは先程と若干異なり、URL Pathに/<int:pk>が記載されている。
これはDBのどのレコードを更新していいか判断できないので<int:pk>で一意にレコードを特定する。
DjangoではデフォルトでTableを作成するとidというPrimary KeyになるColumnが作成される。

samplecrud/views.py

from django.shortcuts import render, redirect
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

def create_page(request):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.create(title=title, memo=memo)
        return redirect('list')
    return render(request, 'create.html')

def update_page(request, pk):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.filter(pk=pk).update(title=title, memo=memo)
        return redirect('list')
    object = Todo.objects.get(pk=pk)
    return render(request, 'update.html', {'object':object})

updateではどのレコードを表示、更新するか特定する必要がある。
object = Todo.objects.get(pk=pk) はPrimary Keyの値を条件にレコードを取得する。

Todo.objects.filter(pk=pk).update(title=title, memo=memo) はPrimary Keyでフィルターし更新する。

templates/update.html

<h1>Update Page</h1>

<form action="" method="POST"> {% csrf_token %}
    <p>
        <label for="id_title">Title:</label>
        <input type="text" name="title" value="{{ object.title }}" maxlength="100" required="" id="id_title">
    </p>
    <p>
        <label for="id_memo">Memo:</label>
        <textarea name="memo" cols="40" rows="10" maxlength="255" required="" id="id_memo">{{ object.memo }}</textarea>
    </p>
    <input type="submit" value="更新する">
</form>

create.htmlと似ているが、{{ object.title }}と{{ object.memo }}でviews.pyで取得したレコードを表示している。

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/update/1 にアクセスすると以下によう表示され、更新するとlistが表示されて先程更新したデータが表示される。
しかしながら、『update/1』のURL Pathに/<int:pk>がに当たる数字を画面上表示していないため、わかりにくいので、 listページからそれぞれのレコードに対してリンクを貼ることにする。

Listページからのリンクを作成

templates/list.html

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>
            {{ item.title }}: {{ item.memo }}
            <a href="{% url 'update' item.pk %}">編集する</a>
        </li>
    {% endfor %}
</ul>

aタグを使い、リンクを作成する。
ここでもurl指定はsamplecrud/urls.pyのnameに指定した名前を利用できる。
また、どのレコードか一意に特定するため、『item.pk』でパラメータをしていることがポイントとなる。

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ を表示すると以下の用になり、クリックするとupdate画面に移動できる。

f:id:n-guitar:20201228214818p:plain:w300

Dalete画面の作成

最後にDBのレコードを削除する画面を作成する。
以下のファイルをそれぞれ編集する。

samplecrud/urls.py

from django.urls import path
from .views import list_page, create_page, update_page, delete_page

urlpatterns = [
    path('list/', list_page, name='list'),
    path('create/', create_page, name='create'),
    path('update/<int:pk>', update_page, name='update'),
    path('delete/<int:pk>', delete_page, name='delete'),
]

updateと同じく、どのレコードかを特定する/<int:pk>を指定する。

samplecrud/views.py

from django.shortcuts import render, redirect
from .models import Todo

def list_page(request):
    object_list = Todo.objects.all()
    return render(request, 'list.html', {'object_list':object_list})

def create_page(request):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.create(title=title, memo=memo)
        return redirect('list')
    return render(request, 'create.html')

def update_page(request, pk):
    if request.method == 'POST':
        title = request.POST['title']
        memo = request.POST['memo']
        Todo.objects.filter(pk=pk).update(title=title, memo=memo)
        return redirect('list')
    object = Todo.objects.get(pk=pk)
    return render(request, 'update.html', {'object':object})

def delete_page(request, pk):
    if request.method == 'POST':
        object = Todo.objects.get(pk=pk)
        object.delete()
        return redirect('list')
    object = Todo.objects.get(pk=pk)
    return render(request, 'delete.html', {'object':object})

deleteの場合は一度削除してよいかもう一度確認する画面を表示する。
object = Todo.objects.get(pk=pk) で特定のレコードを取得し、 object.delete() でレコードを削除する。

templates/delete.html

<h1>Delete Page</h1>

<form action="" method="POST"> {% csrf_token %}
    <p>
        {{ object.title }}{{ object.memo }}
        を本当に削除しますか?
    </p>
    <input type="submit" value="削除する">
</form>

listからdeleteへのリンクもupdate同様作成しておく

<h1>list Page</h1>
<ul>
    {% for item in object_list %}
        <li>
            {{ item.title }}: {{ item.memo }}
            <a href="{% url 'update' item.pk %}">編集する</a>
            <a href="{% url 'delete' item.pk %}">削除する</a>
        </li>
    {% endfor %}
</ul>

この状態でもう一度runserverし

$ python manage.py runserver

http://127.0.0.1:8000/samplecrud/list/ を表示すると以下の用になり、削除をクリックするとレコードが削除される。

f:id:n-guitar:20201228215828p:plain:w300
f:id:n-guitar:20201228220915p:plain:w300
f:id:n-guitar:20201228220933p:plain:w300

以上、Djangoのgeneric viewを利用してCRUDを行う方法でした。