SE(たぶん)の雑感記

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

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

前回記事で、Djangoでちゃちゃっとツール作った、みたいな話を書きました。

hiroronn.hatenablog.jp

開発では、Djangoのテンプレートビューを多用してささっと機能を増やしていく、という手法を用いました。

テンプレートビューに関して、簡単ではありますが紹介するのが、今回の記事です。

同等の機能は、他のMVCモデルでも存在すると思いますので、こういう感じなんだなーと思っていただければ幸いです。

バージョン等

当記事の記述は、Python等の以下のバージョンでの動作検証に基づき、執筆しています。

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

ジェネリックビューとは

そもそも、DjangoViewという場合、MVCモデルのControllerを指します。紛らわしい。Djangoやっていない人はお手数ですが都度読み替えてください。当記事中ではViewと表記します。

Viewの役割は、リクエストに対してレスポンスを返すことです。ただ、ページを返す場合の応答は定型化できます。例として、

  • リストを返す
  • 単一のオブジェクトを返す
  • 作成する
  • 更新する
  • 削除する

などが挙げられます。

Djangoでは、上のような処理を行う際、URLの引数、対象Modelなどの情報を指定することで、上記処理を行ってくれるようなViewが定義されています。それをジェネリックビュー(Generic View)と呼んでいます。

前提: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を使います。

リストを返す(ListView)

結果を一覧として返します。もっとも単純な場合、フィルタ等なしで単に一覧を返す形となります。

Tableの一覧

  • views/table.py
from django.db.models import Count
from django.views.generic import ListView
from ..modesl import Table

class TableListView(ListView):
    """テーブル一覧"""
    model = Table
    context_object_name = 'tables'
    paginate_by = 50

最も単純な形です。クラスの中で、変数として様々な指定を行います。よく使うのはこんな感じです。

変数名 説明
model Modelのクラスを指定する
context_object_name templateで、modelを参照する場合の名前
paginate_by ページングする場合の、1ページに表示するレコード数。指定した場合、自動的にページングが有効になる
template_name 省略すると、「モデル名(小文字)_list.html」になる

これをもとにtemplateを書きます。

  • templates/table_list.html
{% extends "inputs/base.html" %}
{% block title %}テーブル一覧{% endblock %}
{% block body %}

<h3>テーブルの一覧</h3>
{% include 'inputs/paging.html' %}
<table class="table table-striped table-bordered table-hover table-responsive table-sm">
    <thead>
        <tr>
            <th>テーブル名</th>
        </tr>
    </thead>
    <tbody>
        {% for table in tables %}
        <tr>
            <td>
                <a href={% url 'inputs:table' table.id %}>{{ table.name }}</span>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>

{% endblock %}

base.pyは、<body>以外全部定義したものです。{% for table in tables %}とあるように、Viewcontext_object_name = 'tables'と指定した名称をこちらで使用できます。

ページング処理に関しても、templateに渡されるオブジェクト名等は決まっているため、{% include 'inputs/paging.html' %}のように一つのtemplateにまとめており、都度includeしています。個別にページングを作る必要が無くてとても便利です。

Columnの一覧

ここでは、あるテーブルに属するカラムを取得する、とします。よって、引数等でTableのIDを取得します。

  • views/table.py
from django.views.generic import ListView
from ..modesl import Column

class TableDetailView(ListView):
    model = Column
    context_object_name = 'columns'
    paginate_by = 50
    template_name = 'inputs/columns.html'

Tableとあまり変わりません。しかし、今回は

  • 絞り込みが必要
  • テーブル名を画面に表示

をやりたいとします。この場合、フィールド指定のみでは実現できません。そこで、ListViewからメソッドを継承します。

def get_queryset(self):
    return Column.objects.filter(table=self.kwargs.get('pk'))

def get_context_data(self, *, object_list=None, **kwargs):
    context = super(TableDetailView, self).get_context_data(**kwargs)

    context['table'] = get_object_or_404(Table, id=self.kwargs.get('pk'))

    return context

継承したメソッドは以下の通りです。

メソッド名 説明
get_queryset 取得する一覧のクエリを指定
get_context_data 一覧以外で必要なデータを取得し、割り当てる

一覧は、何もしないとすべてのカラムを取得してしまうため、指定したテーブルに限定しています。

また、カラムが属するテーブル情報が欲しいため、get_context_dataで取得し、contextにセットしています。

上記のメソッドで指定しているpkという値は、URLで指定しています。

  • urls.py
from django.urls import path
from .views.home import HomeView
from .views import table

urlpatterns = [
    path('', HomeView.as_view(), name='home'),
    path('table', table.TableListView.as_view(), name='tables'),
    path('table/<int:pk>', table.TableDetailView.as_view(), name='table'),

path('table/<int:pk>', table.TableDetailView.as_view(), name='table'),のように指定すると、引数でpkが渡ってきます。

詳細を返す(DetailView)

単一項目を返す際に利用します。ここから、更新や削除機能を提供するのが常道と思われます。

Column詳細

  • views/column.py
class ColumnDetailView(DetailView):
    """カラム詳細"""
    model = Column
    template_name = "inputs/col.html"
    context_object_name = 'col'

    def get_queryset(self):
        return super().get_queryset().select_related(
            'table'
        )

ここでも、必要であればメソッド継承を行います。get_context_dataを使う場合、既に対象をオブジェクトを取得しているため、さらに何か取得する必要がある場合、それを参照できます。ここでは、カラム名とそれが所属するテーブルの情報も欲しいため、それを合わせて取得しています*1

def get_context_data(self, **kwargs):
    context = super().get_context_data(**kwargs)

    context['kinds'] = ColumnKind.objects.filter(
        column=self.object
    ).select_related(
        'definition'
    )

    return context

詳細は省きますが、列定義とは別に列分類というものがあり、それを追加取得しています。

上記、filterで参照しているself.objectに、DetailView内で自動で取得されたColumnオブジェクトへの参照が格納されています。改めてColumnを取得する必要がないため、クエリ発行回数の節約になります。

なお、urlは以下のように指定します。

  • urls.py
urlpatterns = [
    path('column/<int:pk>', column.ColumnDetailView.as_view(), name='col'),
]

view内でpkを一度も参照していないことに気づかれたでしょうか?

DetailViewを使う場合、urlに指定した値からうまくオブジェクトを取得してくれます。楽です。

参照リンク

docs.djangoproject.com

おわりに

これらを使うと、Viewがファットになってしまう*2状況は避けられません。しかし、なんでもTemplateViewで書くよりは何をやっているのか明確です。

ここでこの変数を指定したらこの処理やりますよ、というのは、動的言語らしい感じがします。それゆえに楽できるわけですが、複雑になるとカオス感が加速します。

用法用量を守って正しくお使いくださいという言葉が身に沁みます。

個人的な意見を言うなら、フレームワークを使うと確かに楽だけど、アプリを発展させるときに足かせになるのもまた、フレームワークだと思います。その確認も兼ねて、使ってみている最中です。

次回は更新ビューについて書きます。

*1:select_relatedを使うと、ここではクエリ発行回数の節約になる。model定義上紐づけがある場合のみ有効。無くてもtemplateで参照できるが、クエリが発行されてしまう

*2:templateは、コンテキストの名前を変えなければ独立、modelは更新等を自力で書けるので、独立している