SE(たぶん)の雑感記

一応SEやっている筆者の思ったことを書き連ねます。会計学もやってたので、両方を生かした記事を書きたいと考えています。 でもテーマが定まってない感がすごい。

Django - ジェネリックビューで楽してみた(CreateView編)

間が空きましたが、前回記事に引き続き、Djangoジェネリックビューについてお話しします。

今回はCreateViewです。CRUDCに該当します。

以前の記事はこちらです。ジェネリックビューとは何なのか、という部分について説明を書いています。当記事を見る前に前回記事を見ることをお勧めします。

hiroronn.hatenablog.jp

バージョン等(再掲)

基本的に前回と同様です。一部bootstrapを使うので、追加しています。

ツール バージョン
Python 3.7.2
Django 2.1.7
bootstrap 4.1.3

前提:Model層

  • models.py
from django.db import models

class Table(models.Model):
    """テーブル一覧"""
    name = models.CharField(max_length=100)

class Column(models.Model):
    """カラム定義(物理名格納)"""
    table = models.ForeignKey(Table, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)

前回と同様ですが、上記二つのModelを使います。

CreateViewについて

CreateViewとは、名前の通りデータの生成を行うビューです。DBでいうとINSERTを行います。

CreateViewの効果

レコードを1行作ろうとすると、本来なら

  • modelに対応したHTMLとformを作る
  • POSTが来たら、ブラウザで入力された値を取得する
  • 値を検証する
  • 保存modelのインスタンスを作る
  • 入力値を一つずつmodelのフィールドに渡す
  • modelの保存処理を呼び出す
  • ブラウザの画面を遷移させる

という作業が必要です。面倒なのでできる限り省力化しよう、というのがCreateViewの目指すところです。

CreateViewを使うと、最低限やることは

  • 対象modelを指定する
  • 入力したいフィールドを指定する
  • 描画用HTMLを指定する
  • 更新後の遷移先を指定する

だけになります。すべて指定するだけでよいです。

ケース1:単体で完結するモデル

tableの作成

tableは、nameと主キーしかないです。他のテーブルとリレーションが無い場合、最低限の定義だけで保存処理まで作れます。

  • views/table.py
from django.views.generic import CreateView
from django.urls import reverse_lazy

class TableCreateView(CreateView):
    """テーブル作成"""
    model = Table
    fields = ('name', )
    template_name = 'create.html'
    success_url = reverse_lazy('home')

クラス変数は、それぞれこのような意味です。

変数 意味
model Createの対象となるモデルのクラス
fields modelのうち、Create時に入力対象とするフィールド。テンプレートでformが自動生成される
form_class 自動生成されるformではないものを使いたい場合、そのformのクラスを指定する
template_name 入力時に用いるテンプレート名。省略すると[model名]_form.htmlになる
success_url 作成成功時に遷移するページのURL

続いてTemplateです。これはフォームを表示する汎用的なものにしています*1

  • templates/create.html
{% extends "base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block body %}

<h3 class="h3 border-bottom">{{ title }}</h3>

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input class="btn btn-primary" type="submit" value="登録">
</form>

{% endblock %}

見た目はこのようになります。一部bootstrapを使っています。

f:id:hiroronn:20190305074513p:plain

上記nameを入力し、「登録」を押すだけで、Createが行われます。これだけです。クラス作って最低限の定義を行うだけで、Create用のViewが準備できます。すごく便利です。

作成後のデータを開く

上記のままだと、登録後にトップページが開く*2ようになります。

しかし、作成したものを開きたい、とします。その場合は、get_success_urlをオーバーライドします。ただ、その前に遷移先ページを作成します。

  • urls.py

path('table/<int:pk>', table.TableDetailView.as_view(), name='table'),を追加します。

urlpatterns = [
    path('', HomeView.as_view(), name='home'),
    path('table/new', table.TableCreateView.as_view(), name='table_new'),
    path('table/<int:pk>', table.TableDetailView.as_view(), name='table'), # 追加
]

そして、TableDetailViewという詳細ビューを用意します。内容は最初に紹介した記事のDetailViewの内容と同じですので割愛します。そして、table.pysuccess_urlを削除し、代わりにget_success_urlをオーバーライドします*3

from django.views.generic import CreateView
from django.urls import reverse
from ..models import Table

class TableCreateView(CreateView):
    """テーブル作成"""
    model = Table
    fields = ('name', )
    template_name = 'create.html'

    def get_success_url(self):
        return reverse('table', kwargs={'pk': self.object.id})

reverseの引数は、

順番 内容
第一引数 urls.pyで指定したname
kwargs(名前付き引数) URLに渡す引数

となります。追加したURLと変数<int:pk>(<型:変数名>)に合わせた定義です。こう書くと、Createの後に作成されたオブジェクトのページに遷移します。

reverseのヘルプ

なお、get_success_urlself.objectというものを参照しています。これに関しては、

CreateView を使うとき、self.object にアクセスできます。これは作成されているオブジェクトです。オブジェクトがまだ作成されていない場合、値は None になります。

という記述が以下のページにある通り、生成後のオブジェクト(ここではmodelに指定したTableインスタンス)が格納されています。なので、CreateViewで生成後のオブジェクトを元に処理したい場合、同じ手法が使えます。

docs.djangoproject.com

ケース2:リレーションを持つモデル

columnの作成

columnは、tableを参照する外部キー制約があります。普通にフォームを作ると、

  • tableの選択
  • nameの入力

という形になりますが、columnは普通テーブルに追加するので、テーブルの画面から「列の追加」ボタンがある感じで作ります。

  • urls.py

path('table/<int:pk>/column/new', column.ColumnCreateView.as_view(), name='col_new'),という、TableDetailViewから呼ばれることを前提としたURLを用意します。

urlpatterns = [
    path('', HomeView.as_view(), name='home'),
    path('table/new', table.TableCreateView.as_view(), name='table_new'),
    path('table/<int:pk>', table.TableDetailView.as_view(), name='table'),
    path('table/<int:pk>/column/new', column.ColumnCreateView.as_view(), name='col_new'), # 追加
]
  • views/column.py
from django.views.generic import CreateView
from django.shortcuts import get_object_or_404
from ..models import Table, Column

class ColumnCreateView(CreateView):
    model = Column
    fields = ('name', )
    template_name = 'create.html'

    def get_success_url(self):
        return reverse('inputs:table', kwargs={'pk': self.kwargs.get('pk')})

まずは、先ほどと同じく最低限で定義します。

これで登録しようとすると、

f:id:hiroronn:20190308202231p:plain
例外

例外が発生します。

NOT NULL constraint failed

とあるように、外部キーであるtableを指定していないために発生します。なので、事前に渡します。

更新前に、作成されようとしているオブジェクトに値を渡すには、form_validをオーバーライドします。

  • views/column.py
from django.views.generic import CreateView
from django.shortcuts import get_object_or_404
from ..models import Table, Column

class ColumnCreateView(CreateView):
    model = Column
    fields = ('name', )
    template_name = 'create.html'

    def form_valid(self, form):
        # テーブルを置く
        table = get_object_or_404(Table, pk=self.kwargs.get('pk'))
        form.instance.table = table

        return super().form_valid(form)

    def get_success_url(self):
        return reverse('inputs:table', kwargs={'pk': self.kwargs.get('pk')})

上記に書いていますが、form_validの引数formにはinstanceという項目があり、ここに作成しようとしているオブジェクトが入っています。

なので、そこにセットした後に継承元のform_validを呼び出すと、保存が行われます。

そもそも、このタイミングでやるのが適切か?とは思いましたが、

Saves the form instance, sets the current object for the view, and redirects to get_success_url().

Google翻訳:フォームインスタンスを保存し、現在のオブジェクトをビューに設定して、get_success_url()にリダイレクトします。)

という記述がここにあることから、おそらく問題はないのだと思います。

余談:保存処理が呼ばれるタイミング

CreateViewの場合、実際の保存はsuper().form_valid(form)のタイミングで行われています。

継承元のソースを追っていくと、

  • edit.py
class ModelFormMixin(FormMixin, SingleObjectMixin):
    # 中略
    def form_valid(self, form):
        """If the form is valid, save the associated model."""
        self.object = form.save()
        return super().form_valid(form)

というメソッドがあり、保存処理が呼ばれていることが分かります。

おわりに

CreateViewが有効に使えるのは、単一のmodelに対して保存する場合です。その場合は上に書いたような、簡単な定義だけで動かせてしまいます。

追加データを取るのは、display viewと同じくget_context_data等で行えますし、保存前の処理も変更できます。ある程度は柔軟に使えます。

ただ、多くの入力項目があり、複数のテーブルに書き込む必要があるページの場合、色々大変そうです。今回は調べていませんが、FormSetなるものもあり、複数のformを扱うこともできるようです。

便利な使い方はまだまだありそうです。

次回は、UpdateViewについて述べます。またよろしくお願いします。

*1:サクッと作るため。入力しやすさを追い求めていない

*2:設定による

*3:VSCodeの場合、メソッド名を入力すると候補に表れ、選択すると自動的にオーバーライドされる