クラスベースのView

クラスベースのView

前章で扱ったdefで書く関数ベースのViewとは違い、classで記述する方法を、クラスベースビュー または クラスベース汎用ビュー と呼びます。

汎用とは英語でGenericと言い、Djangoのdjango.generic.viewsから様々な汎用ビューをインポートして、classでViewを作成していきます。

Djangoは公式ドキュメントでもクラスベースビューを推奨しています。
理由は、同じコードを書かない(DRY)というDjangoの思想に基づいているもので、
継承とミックスインを用いれば、同じコードを書くケースを減らせるというもの。
現在、defの関数ベースでViewを書いている場合は、今後少しづつクラスベースのViewを導入していけると良いでしょう。

関数ベースとクラスベースの比較

関数ベースで以下のようなコードがあるとします。

from django.http import HttpResponse

def index(request):
    if request.method == 'GET':
        print('GETリクエスト')

        return HttpResponse(200)

    if request.method == 'POST':
        print('POSTリクエスト')

        return HttpResponse(200)

クラスベースで書くと以下のように記述できます。

from django.http import HttpResponse
from django.views import View

class IndexView(View):

    def get(self, request):
        print('GETリクエスト')

        return HttpResponse(200)

    def post(self, request):
        print('POSTリクエスト')

        return HttpResponse(200)

コードを比較すればわかると思いますが、クラスベースで書く方が圧倒的に処理がわかりやすいです。
さらにクラスで書くことで、クラス内関数や変数なども記述できるので、使用するメソッドなどをクラスごとに分けることも可能になってきます。

クラスベースビューは、django.viewsから Viewをインポートしてクラスの引数に渡すことで使用できますが、View以外にもクラスベース汎用ビューと呼ばれるものがあり、CRUDと呼ばれるデータベースの作成・取得・更新・削除などの処理に特化したビューが存在します。
それらを適切に用いることで、Djangoのプロジェクト開発をスムーズにかつスピーディーに進めることができます。

クラスベース汎用ビュー

今後、クラスベース汎用ビューもまとめてクラスビューと呼びますが、
まずはどういったものがあるのか主要なものをまとめてみました。

クラスベースビュー一覧

  • TemplateView
    基本的なページビュー
  • CreateView
    データの作成のためのビュー
  • UpdateView
    データの更新のためのビュー
  • DeleteView
    データの削除のためのビュー
  • ListView
    複数のデータを取得するビュー
  • DetailView
    一つのデータを取得するビュー

まずはTemplateViewから見ていきましょう。

TemplateView

TemplateViewは最も基本的なクラスビューで、たった数行でページのバックエンドを組むことができます。

views.py

from django.views.generic import TemplateView

class IndexView(TemplateView):

    template_name = 'index.html'

index = IndexView.as_view()

これでindexをurls.pyに記述するだけで、index.htmlの内容をページに反映させることができます。

全てのクラスビューには、オーバーライドできるデフォルトの関数があります。
例えば、上記のコードでデータを追加でフロントエンドに投げたい場合、get_context_dataという関数を追加することでそれを実現できます。

views.py

from django.views.generic import TemplateView

class IndexView(TemplateView):

    template_name = 'index.html'

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

        context['hoge'] = 'Hello world!'
        return context

index = IndexView.as_view()

index.html{{ hoge }} と入力してみてください。
Hello world!とページに表示されるはずです。

基本的に全てのDjangoのページには context という辞書型のデータがバックエンドから投げられています。
get_context_dataは、その辞書の中に好きなデータを投入することができます。

他にも以下のような関数が主にオーバーライドされることが多いです。

  • get
  • post
  • dispatch

getやpostは既に前述したように、GETやPOSTなどのリクエストに応じて、処理を加えることができます。
dispatchは、ページにリクエストがきた際にそれをGETやPOSTに振り分ける処理のことです。
つまり、dispatchは、getやpostの処理が呼ばれる前に呼ばれる処理です。
これをオーバーライドすることで真っ先に必要な処理を行うことができます。

views.py

from django.views.generic import TemplateView
from django.http import HttpResponse

class IndexView(TemplateView):

    template_name = 'index.html'

    def dispatch(self, request, *args, **kwargs):
        print('DISPATCH')
        return super().dispatch(request, *args, **kwargs)

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

        context['hoge'] = 'Hello world!'
        return context

    def get(self, request, *args, **kwargs):
        print('GET')
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        print('POST')
        return HttpResponse(200)

index = IndexView.as_view()

出力(GETリクエスト)


DISPATCH
GET

CreateView

TemplateViewを使えば、ほぼ全てのページをクラスベースで開発していくことができます。
ただし、formを使ったデータの保存処理を全てpostに記述していくのは面倒です。
データの作成にはCreateViewを使うことでコードの記述量を減らすことが可能です。
models.pyにArticleというclassを記述しているとしましょう。

views.py

from django.views.generic import CreateView
from django.urls import reverse_lazy

from .models import Article

class ArticleCreateView(CreateView):

    template_name = 'article/create.html'
    model = Article
    fields = ['title', 'body']
    success_url = reverse_lazy('sample_app:index')

article_create = ArticleCreateView.as_view()

template_nameはTemplateViewと同じです。
modelには、該当するデータのクラス名を記述します。(models.pyからインポートします。)
fieldsには、リストかタプルで扱うフィールドを指定します。
success_urlには保存処理が成功した後の遷移先のURLを記述します。

なおarticle/create.htmlにて{{ form }}と記述すると自動でフォームが生成されます。
POSTのフォームで囲い、送信ボタンを設置することで保存処理の一連の流れを実装することができます。

初めてみた方はこれだけ?と驚かれるかもしれませんが、本当にこれだけでデータが保存されます。
実際には、form_validという関数が全ての処理を行ってくれています。
form_validgetpostなどのようにオーバーライドすることができます。

views.py

from django.views.generic import CreateView
from django.urls import reverse_lazy

from .models import Article

class ArticleCreateView(CreateView):

    template_name = 'article/create.html'
    model = Article
    fields = ['title', 'body']
    success_url = reverse_lazy('sample_app:index')

    def form_valid(self, form):
        article = form.save(commit=False)
        article.creator = self.request.user.username
        article.save()
        return super().form_valid(form)

article_create = ArticleCreateView.as_view()

ここでは、保存処理が走る前に、creatorフィールドにユーザーネームを入れて保存処理を入れました。
form_validは、CreateView, UpdateView, FormViewにて使用することができます。

UpdateView

UpdateViewはCreateViewとほとんど同じです。
違いはデータの作成ではなく、既存のデータの更新ということ。

views.py


from django.views.generic import UpdateView
from django.urls import reverse_lazy

from .models import Article

class ArticleUpdateView(UpdateView):

    template_name = 'article/update.html'
    model = Article
    fields = ['title', 'body']
    success_url = reverse_lazy('sample_app:index')

article_update = ArticleUpdateView.as_view()

使い方はほとんど同じです。
update.htmlにて、{{ form }}と入力するとCreateViewと同じようにフォームが自動生成されますが、違うのはそのフォームのinputの中に既存のデータが入っています。
また保存処理が走った際もデータが新しく作られることはなく、既存のデータが更新されます。

そのためにURLにidやそのほかのフィールドを付け加える必要があります。

urls.py

path('article/<int:pk>/update', views.article_update, name='article_update),

上記のpkとはPrimary Keyでidのことです。intはIntegerでデータの型を指定しています。
UpdateViewでは、このidから内部的に既存のデータを取得してきて、更新処理を行います。

DeleteView

あまり使われることは少ないかもしれませんが、DeleteViewというものも存在します。
これはデータの削除に使われるクラスビューです。

views.py

from django.views.generic import DeleteView
from django.urls import reverse_lazy

from .models import Article

class ArticleDeleteView(DeleteView):

    template_name = 'article/delete.html'
    model = Article
    success_url = reverse_lazy('sample_app:index')

    # delete処理をオーバーライドできる
    def delete(self, request, *args, **kwargs):
        print(self.object.title)
        return super().delete(request, *args, **kwargs)

article_delete = ArticleDeleteView.as_view()

UpdateViewやCreateViewと違い、フォームにフィールドはないのでfieldsは指定しませんが、リクエストを扱うので、POSTフォームをフロントで作成する必要があります。
POSTリクエストが送られると自動で該当するデータを削除します。
そのため、UpdateViewと同じようにURLに<int:pk>のようにidなどの一意なデータを加える必要があります。

またdef delete()をオーバーライドすることで削除する際の処理をカスタマイズすることも可能です。

ListView

ListViewは複数のデータを取得するクラスビューです。
Djangoでは、複数のデータのセットのことをクエリセットといいます。
クエリセットはリストのようなデータの型になっており、データが存在しなくてもエラーにならないのが特徴です。
ListViewではこのクエリセットを取得します。

views.py

from django.views.generic import ListView

from .models import Article

class ArticleListView(ListView):

    template_name = 'article/list.html'
    model = Article
    context_object_name = 'articles'
    paginate_by = 10

article_list = ArticleListView.as_view()

デフォルトでは、article/list.html{{ object_list }}でこのデータを表示することができます。
上記のコードでは、context_object_namearticlesに指定しています。
こうすることで、{{ object_list }}の代わりに {{ articles }}でデータを表示することができます。
ListViewの特徴は簡単にページネーションを実装できることにあります。

上記のコードでpaginate_byに10を指定しています。
これにより、取得するデータを10に制限しています。
DjangoのTemplate機能を使うことで、ページネーションも簡単に実装することができます。
逆にこれをしない場合、データが10000などになってくると10000のデータを一度で取得することになるので、ページの読み込み速度が著しく遅くなってしまいます。

本来は面倒くさくなるページネーション実装ですが、ListViewを使うことですごく簡単に組み込むことができます。

Bootstrapを併用したページネーションコードの一例

article/list.html

<nav aria-label="Page navigation">
  <ul class="pagination justify-content-center">
    {% if page_obj.has_previous %}
    <li class="page-item">
      <a
        class="page-link"
        href="?page={{ page_obj.previous_page_number }}"
        aria-label="Previous"
      >
        <span aria-hidden="true">&laquo;</span>
        <span class="sr-only">Previous</span>
      </a>
    </li>
    {% endif %}
    {% for num in paginator.page_range %}
      {% if num <= page_obj.number|add:5 and num >= page_obj.number|add:-5 %}
        {% if page_obj.number == num %}
          <li class="page-item active">
            <span class="page-link">
              {{ num }}
              <span class="sr-only">(current)</span>
            </span>
          </li>
          {% else %}
          <li class="page-item">
            <a class="page-link" href="?page={{ num }}">{{ num }}</a>
          </li>
        {% endif %}
      {% endif %}
    {% endfor %}
    {% if page_obj.has_next %}
      <li class="page-item">
        <a
          class="page-link"
          href="?page={{ page_obj.next_page_number }}"
          aria-label="Next"
        >
          <span aria-hidden="true">&raquo;</span>
          <span class="sr-only">Next</span>
        </a>
      </li>
    {% endif %}
  </ul>
</nav>

TemplateViewでも述べましたが、このListViewでも getpostget_context_dataなどを扱うことができます。
追加で処理が必要な場合これらを組み込んでいくと良いでしょう。

DetailView

今回のクラスビューの最後に扱うのがDetailViewです。
DetailViewでは、クエリセットを扱うListViewとは違い、単体のデータ: オブジェクト を扱うためのビューです。
オブジェクトを扱うビューは UpdateViewDeleteViewで触れました。
つまり、DetailViewでも、urls.pyに<int:pk>などの一意なデータを記述する必要があります。
つまりListViewが記事の一覧ページにあたり、DetailViewが記事を読むページになります。

views.py

from django.views.generic import DetailView

from .models import Article

class ArticleDetailView(DetailView):

    template_name = 'article/detail.html'
    model = Article
    context_object_name = 'article'
    slug_field          = 'slug'
    slug_url_kwarg      = 'slug' 

article_detail = ArticleDetailView.as_view()

新しくslug_fieldslug_url_kwargが追加されています。
例えば、URLでidではなく、slug(URLスラッグ)を使いたい場合、このように記述します。
slug_fieldで該当するフィールド名を、
slug_url_kwargでurls.pyに記述する名前を記入しています。

これによりURLが

/article/1
ではなく
article/first-post
などのように表示されます。

他の変数は全て既に扱ったとおりです。
ここでは、context_object_nameでデフォルトでは {{ object }}となるデータ名を {{ article }} に変更しています。

さいごに

Djangoのクラスビューは、デフォルトで記述されているものをオーバーライドして使用しています。
つまり仮想環境にインストールしているDjangoのコードを全てみていけばその内容が全て書かれています。

今回扱ったビュー以外にも日付を扱うものであったり、フォームを扱うものであったり様々なビューが存在します。
今回扱ったもの以外は、必要になった時に調べて使っていければ良いでしょう。
CRUD処理を適切に使い分けることで。Djangoの開発はよりスムーズになります。
さらに別の人がコードを見た時に、簡単に処理を見分けることができコード全体の可読性がとても高くなります。
少しづつ導入できると良いでしょう。