【Django入門14】クラスベースビューの基本

クラスベースビューの解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django のビューの1つの種類であるクラスベースビューについて解説していきます!

クラスベースビュー

まずは、クラスベースビューとは何者なのか?この点について解説していきたいと思います。

クラスベースビューとは

クラスベースビューとは、ビューの種類の1つです。ビューは、Django の基本構成となる MTV における V となります。

そして、クラスベースビューとは、クラスの定義によって作成されるビューのことを言います。

その他のビューの種類としては「関数ベースビュー」が挙げられます。この Django 入門の連載において今まで扱ってきたビューは関数ベースビューとなります。

スポンサーリンク

クラスベースビューと関数ベースビューの違い

では、このクラスベースビューと関数ベースビューの違いとは何なのでしょうか?

ビューの作り方が異なる

この2つではビューの作り方、言い換えれば views.py に定義する対象が異なります。

関数ベースビューでは、views.py に関数を定義することでビューを作成していくことになります。それに対し、クラスベースビューの場合は、前述の通り views.py にクラスを定義することでビューを作成していくことになります。要は、関数ベースビューとは関数のビューであり、クラスベースビューとはクラスのビューとなります。

もっと詳細に言えば、関数ベースビューの場合、views.py へ定義する関数に処理を実装していくことでビューを作成していきます。クラスベースビューの場合、views.py に Django フレームワークが提供するクラスのサブクラスを定義し、そのサブクラスのカスタマイズを行うことでビューを作成していきます。

関数ベースビューとクラスベースビューの作り方の違い

このカスタマイズは、クラス変数の定義メソッドのオーバーライドによって実現されます。オーバーライドを行う場合はクラスベースビューでも処理を実装することになりますが、それもサブクラスのカスタマイズの一種であると捉えていただければ良いです

関数ベースビューの例

次は簡単な例を確認しながら、関数ベースビューとクラスベースビューの違いについて考えていきたいと思います。

例えば下記の views.py における comments_view は関数を定義することで作成されているため、この comments_view は「関数ベースビュー」であると考えられます。

関数ベースビューのviews.py
from django.shortcuts import render
from .models import Comment

def comments_view(request):
    comments = Comment.objects.all()

    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)

この comments_view 関数は下記ページの 掲示板アプリでモデルを利用してみる で示した views.pycomments_view 関数と同じものになります。

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

この comments_view 関数はリクエストを受け取り、Comment.objects.all() により Comment というモデルクラス(テーブル)から全インスタンス(レコード)を取得するクエリを生成し、それをテンプレートファイルから comments という名前で参照できるようにコンテキスト context にセットします。

さらに、comments_view 関数は引数にテンプレートファイル 'forum/comments.html' とコンテキスト context  を指定して render 関数を実行し、その実行結果を return するようになっています。これにより、render 関数でテンプレートファイルとコンテキストから HTML が生成され、その結果がレスポンスとして Django フレームワークに返却されることになります。あとは、そのレスポンスを Django フレームワークがクライアントに返却し、このレスポンスの返却により、クライアント側でページの表示が行われることになります。

関数ベースビューにおけるcomments_viewの説明図

このサイトの Django 入門の連載を読んでくださっている方であれば、お馴染みの処理の流れになると思います。ここで重要なポイントは、前述の通り、上記の views.py のような関数ベースのビューにおいては実現したい処理の流れを実装する必要があるという点になります。

クラスベースビューの例

さて、先ほど示した comments_views は関数ベースビューとなります。それに対し、comments_views をクラスベースビューに置き換えた場合の views.py は下記のようになります。この views.py における CommentList は、先ほど示した関数ベースビューの comments_view と同じページの表示を実現するクラスとなります。

クラスベースビューのviews.py
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'

ご覧の通り、関数ベースビューの views.py とは全く作りが異なります。クラス変数の定義を行なっているだけで、処理などは記述されていません。ですが、この CommentList では、前述で示した comments_view と同じページの表示を実現することが可能です。なぜ、そのようなことが可能なのでしょうか?

答えは非常にシンプルで、上記の CommentList が継承している ListView が、comments_view と同じような処理を行うように実装されているからです。ListView は Django フレームワークで定義されている View のサブクラス となります。この ListView は get メソッドを備えており、その get メソッドが実行された時に、comments_view と同じような処理が行われるようになっています。この処理を図で表したのが下図となります。

ListViewの処理の流れの説明図

MEMO

誤解を与えると良くないので補足しておきますが、この図は大雑把に ListView における get メソッドの処理の流れを表したものであり、細かく言えば、このメソッドの処理の流れはもっと複雑です

例えば、④で示した処理は ListView における get メソッドが終了した後に実行されるようになっています

が、まずは大雑把に上記のような処理の流れを捉えていただいた方が、関数ベースビューとクラスベースビュの違いや関係性について理解しやすいと思います

この図の青字で示した modelcontext_object_nametemplate_nameListView のクラス変数となります。そして、ListView を継承したクラスから、これらのクラス変数の定義を上書きすることができるようになっています。

また、ListView を継承することで、先ほど処理の流れを示した get メソッドも CommentLis に継承されることになります。そして、この get メソッドはクラス変数を参照し、それらのクラス変数の定義に応じて動作するようになっています。そのため、クラス変数の定義を変更してやれば、それに伴って get メソッドの処理・動作が変化することになります。

例えば、model = Comment と定義しておけば、②では Comment.objects.all() が実行されることになります。model = User と定義しておけば、②では User.objects.all() が実行されることになります。

したがって、下記のように ListView を継承する CommentList クラスを定義すれば、

クラスベースビューのviews.py
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'

CommentList クラスでは、modelCommentcontext_object_name'comments'template_name'forum/comments.html' として扱われるようになるため、この CommentListget メソッドの処理の流れは下図のようになります。

CommentListの処理の流れの説明図

そして、この処理の流れは comments_view と同様のものであることが確認できると思います。つまり、ListView には comments_view で行っていたものと同様の処理が既に定義されており、その処理をクラス変数によってカスタマイズできるようになっています。上記の CommentList は、クラス変数を変更することで comments_view と同様の処理を実現するためのカスタマイズ例の1つとなります。

是非ここで覚えておいていただきたいのが、Django フレームワークには関数ベースビューで実装される典型的な処理の流れを実現するためのクラスが既に数多く用意されているということです。これらは全て View のサブクラスとして定義されており、その例の1つが ListView となります。実際には、これらのクラスは単純に View を継承しているだけでなく、様々なクラスを継承して実現されているのですが、最終的には View を継承しています。そして、こういった View を継承しているクラスのことを、このサイトでは View のサブクラス と呼ばせていただきます。

Viewのサブクラスの説明図

例えば、特定のモデルクラス のインスタンスを全て取得し、それらを 特定のテンプレートファイル に埋め込んで表示するような処理は “関数ベースビューで実装される典型的な処理の流れ” と言ってよいでしょう。皆さんもそんなビューを実装したことがあるのではないでしょうか?

まさに、その例の1つが comments_view となります。

そして、このような典型的な処理の流れは Django フレームワークで ListView によって既に実装されています。そして、この ListView のサブクラスを定義してカスタマイズしてやることで、特定のモデルクラス の部分や 特定のテンプレートファイル の部分を好きなように変更したビューを実現することができます。

Viewのサブクラスを継承するクラスでクラス変数の定義を行う様子

“関数ベースビューで実装される典型的な処理の流れ” は他にもあって、例えば 特定のモデルクラス における 特定の1つのインスタンス の情報を 特定のテンプレートファイル に埋め込んで表示するような処理もそれに当てはまると思います。このような処理の流れは View のサブクラス の1つである DetailView で実装されています。他にも、こういった様々な典型的な処理の流れを実現するためのクラスが Django フレームワークには用意されています。それらの紹介は View のサブクラスの種類 で行います。

まず、ここで覚えておいていただきたいことは、そういったクラスのサブクラスの定義とカスタマイズによって様々なビューを実現することが可能となるという点、さらに、サブクラスの定義とカスタマイズというやり方で作成していくビューがクラスベースビューであるという点の2つになります。

クラスベースビューの作り方

続いて、クラスベースビューを作成する手順について解説していきます。

View のサブクラス を継承するクラスを定義する

クラスベースビューを作る上で最初に行うことは、Django フレームワークに用意された View のサブクラス を継承するクラスの views.py への定義となります。

前述で “関数ベースビューで実装される典型的な処理の流れを実現するためのクラスが既に数多く用意されている” と説明しました。これらのクラスは全て、Django フレームワークで View のサブクラス として定義されています。

これらの多くは django.views.generic で定義されていますので(例外もあります)、django.views.generic から実現したいビューに応じたサブクラスを import し、そのクラスを継承するサブクラスを定義します。

クラスの定義
from django.views.generic import Viewのサブクラス

class クラス名(Viewのサブクラス):
    pass

View のサブクラス の代表例の1つは前述でも使用例を示した ListView となります。ListView のサブクラスを CommentList として定義する場合は、まずは下記のように views.py を実装することになります。

クラスの定義例
from django.views.generic import ListView

class CommentList(ListView):
    pass

views.py には、実現したいビューの数だけ上記のようなクラスを定義することになります。その際には、実現したいことに応じて継承する View のサブクラス を適切に選択する必要があります。View のサブクラス の種類の詳細については View のサブクラスの種類 で別途紹介します。

スポンサーリンク

urls.py を作成する

 また、クラスベースビューの場合と関数ベースビューの場合とでは urls.py (アプリ側) の作り方が若干異なりますので、クラスベースビュー用に urls.py を作成する必要があります。

関数ベースビューの場合、urls.py では下記のように urlpatterns を定義し、path の第2引数には、第1引数の URL (URL パターン) へのリクエストをウェブアプリが受け取った際に実行させたい関数オブジェクトを指定する必要がありました。

関数ベースビューの場合のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path(URL, views.関数名),
    略
]

クラスベースビューの場合も基本的な形式は関数ベースビューの時と同様となるのですが、path の第2引数には、第1引数の URL (URL パターン) へのリクエストをウェブアプリが受け取った際に動作させたいクラスの as_view メソッド実行結果を指定する必要があります。つまり、views.クラス名.as_view() を指定する必要があります。関数ベースビューの時と同じノリで views.クラス名 のみを指定しても上手くビューが動作しないので注意してください。

クラスベースビューの場合のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path(URL, views.クラス名.as_view()),
    略
]

クラスベースビューの場合と関数ベースビューの場合との違いは上記の点のみで、クラスベースビューの場合でも path 関数に name 引数等を指定することが可能です。

クラス変数を定義する

続いて、views.py に定義したクラスを “クラス変数の定義” によってカスタマイズしていきます。

クラス変数の定義

このカスタマイズは、通常のクラス変数を定義するときと同様に、下記のように クラス変数名 = 値 といった感じで定義を行うことで実現していくことになります。

クラス変数の定義
from django.views.generic import Viewのサブクラス

class クラス名(Viewのサブクラス):
    クラス変数名1 = 値
    クラス変数名2 = 値

Viewのサブクラス で定義されるクラス変数を上書き

ここで重要な点は2つあって、1つ目はカスタマイズを目的に定義するクラス変数は、継承する Viewのサブクラス で定義されている必要があるという点になります。

これらのクラス変数の定義によるカスタマイズは Viewのサブクラス で定義されているメソッドの動作を変化させるために行います。各種 Viewのサブクラス にはメソッドが定義されており、それらのメソッドの中で、Viewのサブクラス で定義されているクラス変数を参照するようになっていますしたがって、それらのメソッドの中で参照されているクラス変数サブクラスでのクラス変数の定義によって上書きすることで、その上書き後のクラス変数に従って各種 Viewのサブクラス のメソッドが動作するようにカスタマイズすることができます。

Viewのサブクラスから利用されているクラス変数を定義する必要があることを示す図

つまり、Viewのサブクラス で定義されているクラス変数を、その Viewのサブクラス のサブクラス側で定義することで各種メソッドの動作を変化させることができます。逆に、Viewのサブクラス で定義されていないクラス変数を定義したとしても、各種メソッドの動作には影響がないことになります。

そして、Viewのサブクラス ごとに定義されるクラス変数は異なります。

例えば ListView では下記のようなクラス変数が定義されています。したがって、ListView のサブクラスに下記のクラス変数を定義することで、ListView の処理(get メソッド)の動作をカスタマイズすることができます。

  • model:レコードの取得先のテーブル(モデルクラス)を指定
  • queryset:レコードを取得するために発行するクエリを指定
  • ordering:取得するレコードの並びを指定
  • context_object_name:取得したレコードをセットする context のキーを指定(テンプレートファイルから取得したレコードを参照する変数名を指定)
  • pagenage_by*:1ページに割り付けするレコードの数を指定
  • pagenage_orphans*:最後のページに表示される中途半端な数のレコードを前のページに含めるかどうかを指定
  • page_kwarg*:ページ番号を示すクエリパラメータの変数名を指定
  • allow_empty:取得したレコードが 0 であることを許可するかどうかを指定
  • content_type:レスポンスボディのコンテンツタイプを指定
  • template_name:HTML 生成時に使用するテンプレートファイルを指定
  • template_name_suffixtemplate_nameが指定されなかった場合の、HTML 生成時に使用されるテンプレートファイルのファイル名のサフィックス

* を付けたクラス変数はページネーションに関わる設定になります。ページネーションとは多数のレコードを複数のページに割り付けて表示する機能であり、下記ページで解説していますので、詳しく知りたい方は下記ページをご参照ください。

Djangoにおけるページネーションの解説ページアイキャッチ 【Django入門12】ページネーションの基本

前述の通り、これらのクラス変数を ListView のサブクラスに定義することで、ListViewget メソッドの動作を変化させることが可能となります。ですが、上記のクラス変数を定義しても意味のない Viewのサブクラス も存在します。

例えば Viewのサブクラス には DetailView という1つのレコードの詳細情報を表示するクラスが存在します。DetailView は1つのレコードしか扱わないため、ページネーション機能は意味のないものとなります(ページネーション機能は複数のレコードを各ページに割り付ける機能です)。したがって、DetailView には上記で示した pagenage_by などのページネーションに関わるクラス変数は定義されておらず、これらを DetailView のサブクラスに定義しても DetailView のサブクラスの動作は DetailView から変化しないことになります。

このように、カスタマイズを行うためにクラス変数を定義する場合、Viewのサブクラス で定義されているクラス変数を上書きすることが目的となりますので、”Viewのサブクラス で定義されているクラス変数” を定義する必要があります。そして、この定義されているクラス変数は Viewのサブクラス によって異なります。具体的に、各 Viewのサブクラス で定義されるクラス変数の種類については、View のサブクラスの種類 で紹介するリンク先の各ページで紹介を行いたいと思います。

クラス変数のデフォルト設定

2つ目は、Viewのサブクラス を継承するクラスを定義したとしても、そのクラスでクラス変数を定義しない限り、そのクラスは Viewのサブクラス でのクラス変数の定義に従って動作するという点になります。もっと簡単に言えば、Viewのサブクラス を継承するクラスで定義していないクラス変数はデフォルト設定のままとなります。そして、そのデフォルト設定は、Viewのサブクラス で決められています。

例えば ListView における template_name のデフォルト設定は下記のように指定されています。

  • template_name'アプリ名/モデル名_list.html'

ここで モデル名 とはクラス変数 model に指定したモデルクラスの名前を全て小文字にしたものとなります。アプリ名 はアプリの名前で、これも全て小文字となります。

MEMO

もっと正確に言えば、この モデル名 はクラス変数 model に指定したモデルクラスの _meta 属性で指定される名称であり、これを変更している場合は上記のようなデフォルト設定にならないので注意してください

また、クラス変数として model ではなく query_set を定義している場合、その queryset からモデルクラスが特定され、そのモデルクラスの _meta 属性から上記のデフォルト設定が決定されることになります

このように、Viewのサブクラス 側でデフォルト設定が指定されているため、これらのクラス変数の扱いに関しては2つのパターンが存在することになります。

1つ目は、用意するテンプレートファイルやコンテキストに合わせてクラス変数を定義するパターンで、2つ目は、クラス変数を定義せずにデフォルト設定に合わせたテンプレートファイルやコンテキストを用意するパターンとなります。

前者に関しては、ビュー以外に合わせてビューを作る考え方になりますし、後者に関してはビューに合わせてビュー以外を作る考え方となります。

ビューと、ビュー以外の部分の作り方の考え方

先ほどの例で言えば、用意したテンプレートファイルのパスに合わせてクラス変数 template_name を定義しても良いですし、template_name のデフォルト設定のパスにテンプレートファイルを用意するのでも良いです。要は、ビューとビュー以外の部分で話があっていれば良いです。この例の場合は、ビューが利用しようとしているテンプレートファイルのパスと実際に用意されているテンプレートファイルのパスが一致していれば良いです。

ですが、どちらかというと後者の方が良いと思います。なぜなら、それによりビュー以外の部分の作りに自然と一貫性が生まれるからです。例えば CommentList の場合、クラス変数 template_name を定義せずにビューをうまく動作させようとすると、自然とテンプレートファイルの名前が 'forum/comment_list.html' に決まることになります。そして、ある程度 Django に詳しい人であれば、このテンプレートファイルの名前から、そのテンプレートファイルの役割をすぐに理解することができます。この場合は Comment というモデルクラスのインスタンスの一覧を表示するためのテンプレートであることが、 template_name のデフォルト設定の仕組みからすぐに分かります。

ということで、どちらかというとクラス変数を定義せずにデフォルト設定に応じたテンプレートファイルやコンテキストを用意するのが良いかと思います。掲示板アプリのビューをクラスベースに変更する では、既に以前の連載の中でテンプレートファイルなどを用意しているため、用意済みのテンプレートファイルに合わせるようクラス変数の定義を行う例を示しますが、ビューとビュー以外の部分の開発の方針としては上記のように2つのパターンがあって、後者の方がビュー以外の部分の統一感が出てアプリの構成が分かりやすくなることは是非覚えておいてください。

ただし、そもそも定義しないと上手く動作しないクラス変数も存在するので注意してください。例えば ListView の場合は model or queryset のクラス変数を定義しておく必要があります。

メソッドをオーバーライドする

また、クラス変数の定義だけでなく、メソッドをオーバーライドすることでビューのクラスをカスタマイズしていくことも可能です。

Viewのサブクラス には様々なメソッドが用意されています。そして、Viewのサブクラス が行う処理は、これらのメソッドを実行することで実現されています。したがって、これらのメソッドを Viewのサブクラス を継承するクラスでオーバーライドすることで、その処理の動作をカスタマイズすることが可能となります。

メソッドのオーバーライド

このカスタマイズに関しても通常のオーバーライド時と同様の手順で実現することができます。具体的には、サブクラス側にスーパークラス(Viewのサブクラス)の持つメソッドと同じ名前・引数のメソッドを定義してやれば良いだけです。これにより、その名前のメソッドが実行される際にスーパークラス側ではなくサブクラス側のメソッドが呼ばれるようになり、これによって処理の動作をカスタマイズすることができることになります。

メソッドのオーバーライド
from django.views.generic import Viewのサブクラス

class クラス名(Viewのサブクラス):
    クラス変数名1 = 値
    クラス変数名2 = 値

    def メソッド名1(self, 引数1, 引数2, ...)
        略

    def メソッド名2(self, 引数1, 引数2, ...)
        略

Viewのサブクラス で定義されるメソッドを上書き

これは、オーバーライドという言葉を使っているので当たり前になりますが、カスタマイズを目的に定義するメソッドは、継承する Viewのサブクラス で定義されているメソッドである必要があります。

Viewのサブクラスから利用されているメソッドを定義する必要があることを示す図

この定義されているメソッドも Viewのサブクラス によって異なるため、各種 Viewのサブクラス で定義されるメソッドに関しても View のサブクラスの種類 で紹介するリンク先の各ページで紹介を行いたいと思います。

動的に設定を変化させるためにオーバーライドを利用する

このオーバーライドでカスタマイズを行うメリットは、そのクラスの設定を動的に変化させることができるという点にあります。

例えば前述でも紹介した CommentList の例で考えると、ListView には get_queryset というメソッドが定義されているため、この get_querysetCommentList で定義してオーバーライドしてやれば CommentList での get_queryset 実行時の動作を ListView とは異なるものにすることができます。

get_queryset
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'

    def get_queryset(self):
        return Comment.objects.all().order_by('-date')

get_querysetListView によって定義される get メソッドから呼び出しされるメソッドで、データベースに発行するクエリを取得するメソッドとなります。この発行するクエリによって、データベースから取得できるインスタンスが変わることになります。そして、この ListViewget メソッドは CommentList に継承されることになり、CommentList のインスタンスから get メソッドが実行された際には、上記でオーバーライドを行なった CommentListget_queryset が呼び出しされることになります。

そのため、上記のように get_queryset をオーバーライドすることでデータベースに発行するクエリを ListView のものから変化させることができ、CommentList 用にカスタマイズすることが可能となります。上記の場合は、データベースから取得するインスタンスが date フィールドに対して降順にソートされることになります。

ですが、実は上記のように get_queryset をオーバーライドすることはあまり意味がありません。なぜなら、get_queryset が返却する値は毎回同じだからです。毎回 “Comment のテーブルレコードを全て取得して date フィールドに対して降順にソートする” というクエリが返却されるだけです。つまり、この get_queryset が返却する値は静的に決まるということになります。

こういった静的に決まる設定に関しては、メソッドのオーバーライドではなくクラス変数の定義で行うことが可能であるケースが多いです。例えば上記の例であれば、わざわざメソッドのオーバーライドを行わなくても下記のように ordering というクラス変数を定義することで実現可能です(クラス変数 queryset の定義によっても実現可能です)。

orderingの定義でソートを行う例
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'
    ordering = '-date'

クラス変数の定義でもメソッドのオーバーライドによってもカスタマイズ可能なものに関しては、どちらかというとクラス変数の定義で実現する方が良いと思います。なぜなら、処理を記述するとバグが発生する可能性が高くなるからです。ですので、特に静的に決まる設定等のカスタマイズを行う場合はクラス変数の定義で行った方が無難だと思います。

もう少し詳細を説明すると、これらのクラス変数はアプリ起動時に Python によって解釈されることになります。したがって、アプリ起動時に既に決まっている設定等のカスタマイズはクラス変数の定義によって実現することができ、上記の通り、その方が無難です。

ですが、アプリ起動時には設定等が決まらない場合もあります。その場合、クラス変数の定義によるカスタマイズは不可となります。それに対し、メソッドのオーバーライドの場合、アプリ起動時ではなくメソッドが実行されたタイミングで設定を動的に変化させることが可能となります。そのため、メソッドが実行されるタイミングの状況に応じて設定を変更することが可能です。したがって、アプリ起動時には設定等が決まらないカスタマイズ、言い換えれば、設定等を動的に変化させる必要のあるカスタマイズはメソッドのオーバーライドで実現する必要があります。

例えば、ユーザー登録後に、自動的にその登録されたユーザーの詳細ページに遷移させる処理について考えてみましょう!このユーザーの詳細ページの URL パターンは下記のようなものであるとしたいと思います。username は登録されたユーザーのユーザー名とします。そして、ここで実現しようとしているのは、ユーザー登録後の下記 URL へのリダイレクトとなります。

user/<slug:username>/

こういったユーザーの登録はレコードの新規登録と考えられ、このレコードの新規登録を行うのに便利なのが CreateView となります。この CreateView では、ユーザー登録フォームから送信されてきたデータの妥当性の検証を行い、検証結果が OK の場合、そのデータをレコードとしてデータベースに保存し、さらに success_url というクラス変数によって指定される URL にリダイレクトするようになっています(”リダイレクトする” とは、ウェブアプリ側からすれば “リダイレクトレスポンスを返却すること” となります)。

success_urlへのリダイレクトを行う様子

ですので、CreateView のサブクラス側で success_url をクラス変数として定義してやれば、ユーザー登録後に、success_url  の定義に応じた URL にリダイレクトすることができるようになります。

ですが、先ほど示した URL において、username の部分はユーザーによって登録されたユーザーの username (ユーザー名) によって変化することになります。ユーザーがどんな username をつけるかは、実際にユーザー登録されるタイミングでしか分かりません。したがって、アプリ起動時にリダイレクト先の URL を決定することは不可能です。つまり、こういったリダイレクトはクラス変数の定義では実現できません。

こんな時に利用するのがメソッドのオーバーライドによるカスタマイズになります。アプリ起動時にリダイレクト先の URL を設定することはできませんが、ユーザー登録後であれば登録されたユーザーの username は既に分かっているため、リダイレクト先の URL を設定することができます。したがって、ユーザー登録後、すなわちレコードのデータベースへの新規登録後に実行されるメソッドの中であれば、リダイレクト先の URL を username に応じたものに設定することが可能となります。

そして、CreateView では下記の get_success_url メソッドが定義されています。この get_success_url は、クラス変数 success_url が定義されている場合、その success_url を適切なフォーマットに変換して返却するようなメソッドになっています。さらに CreateView は、レコードの新規登録後に、この get_success_url を実行して返却された URL にリダイレクトするようになっています。

get_success_url
def get_success_url(self):
    """Return the URL to redirect to after processing a valid form."""
    if self.success_url:
        url = self.success_url.format(**self.object.__dict__)
    else:
        略
    return url

前述の通り、この get_success_url はレコードのデータベースへの新規登録後に実行されるため、このメソッドの中で登録されたレコードの username を取得し、それを URL にセットして返却するようにすれば、登録されたユーザーの詳細ページへのリダイレクトが実現できることになります。

より具体的には、CreateView のサブクラス側で get_success_url を定義し、下記のように実装してやれば良いことになります。

urlの動的な指定
def get_success_url(self):
    return reverse('user', kwargs={'username':self.object.username})

この get_success_url に関しては CreateView の解説ページで詳しく説明しますが、ここで理解しておいていただきたいのは、動的にパラメーターを変化させたい場合はクラス変数の定義ではなくメソッドのオーバーライドによってそれを実現する必要があるという点になります。クラス変数の定義でもメソッドのオーバーライドでも、どちらでもカスタマイズを行うことができるのであれば、前述でも説明したようにクラス変数の定義で実現するのが良いと思います。

まずは、ここで説明したことを基準に、クラス変数の定義 or メソッドのオーバーライドのどちらでカスタマイズするのかを決めるので良いと思います。ただし、そもそもクラス変数の定義ではカスタマイズ不可の場合もあるため、その場合はメソッドのオーラーライドによってカスタマイズを行う必要があります。メソッドのオーバーライドによるカスタマイズの方がカスタマイズ可能な範囲が広いです。

スポンサーリンク

Mixin で機能を追加

ここまで紹介してきたクラス変数の定義メソッドのオーバーライドによって、継承する Viewのサブクラス からカスタマイズしていくことでウェブアプリ特有のビューを実現していくのがクラスベースビューの作り方の基本的な考え方になります。

ただ、これらのクラス変数の定義やメソッドのオーバーライドだけでなく、Mixin を利用してクラスベースビューに機能を追加する方法もあるので、これについても簡単に説明しておきます。

例えば、下記ページではログイン機能の実現方法について解説しています。

Djangoでのログイン機能の実現方法解説ページアイキャッチ 【Django】ログイン機能の実現方法(関数ベースビュー編)

このログイン機能を実現する際には、ログインを実現するだけでなく、ログインしていないユーザーからのアクセスを禁止することも必要であると説明しました。そして、その際には関数ベースビューの場合、@login_required を関数に指定する必要があると説明しました。

例えば下記のように @login_required を指定することで、非ログインユーザーからのリクエストによって comments_view が実行されようとする際に、強制的に他のページ(例えばログインページ)にリダイレクトされるようになります。したがって、非ログインユーザーは comments_view の実行によって表示されるページにはアクセスできないことになります。

関数への@login_requiredの指定
@login_required
def comments_view(request):
    # 略

ただし、この @login_required は関数に指定可能なものであって、クラスには指定不可です。そのため、クラスベースビューの場合は @login_required の利用は不可ということになります。したがって、クラスベースビューの場合は別の手段で非ログインユーザーからのページのアクセスを防ぐ必要があります。

この例で言えば、その別の手段とは “LoginRequiredMixin を継承する” になります。例えば下記のように LoginRequiredMixin を継承すれば CommentList はログイン中のユーザーからのリクエスト時のみ実行されるようになり、それ以外のユーザーからのリクエストの場合は別のページへリダイレクトされることになります。つまり、CommentList によって表示が実現されるページはログイン中ユーザーからしかアクセスできないことになります。

LoginRequiredMixin
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

class CommentList(LoginRequiredMixin, ListView):
    pass

このように、Mixin を継承することでクラスのカスタマイズを行うことができる点も是非覚えておいてください。

View のサブクラス の種類

続いて、Django フレームワークに用意された Viewのサブクラス を紹介していきます。前述の通り、クラスベースビューは Viewのサブクラス を継承してカスタマイズしていくことで作成していくことになります。ただし、Django フレームワークに用意されている Viewのサブクラス には様々なものが存在しており、これらの中から実現したいビューに合わせて適切な Viewのサブクラス を選択して継承する必要があります。

そのため、各種 Viewのサブクラス の特徴はしっかり覚えておいた方が良いと思います。

ここでは、Viewのサブクラス の種類とともに、これらの特徴について簡単に説明していきます。各 Viewのサブクラス の詳細についてはリンク先のページで別途詳細を解説していますので、必要に応じてこれらのページも参照していただければと思います。

ListView

まず最初に紹介するのが ListView になります。ここまでの説明の中でも何回か登場しましたが、ListView は特定のモデルクラス(テーブル)のインスタンス(レコード)の一覧を表示するビューを作る時に継承する Viewのサブクラス となります。

例えばコメント一覧、ユーザー一覧などを表示するビューは ListView を使えば簡単に実現可能です。

ListViewで実現できるページの例

ListView の詳細に関しては下記ページで解説を行なっています。

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現)

スポンサーリンク

DetailView

ListView が一覧を表示する際に継承されるのに対し、DetailView は特定のモデルクラスの1つのインスタンスの詳細を表示する際に継承される Viewのサブクラス となります。

例えば、特定のユーザーの詳細を表示するビューなどは DetailView を継承することによって簡単に実現することができます。

DetailViewで実現できるページの例

DetailView の詳細に関しては下記ページで解説を行なっています。

DjangoのDetailViewの解説ページアイキャッチ 【Django】DetailViewの使い方(クラスベースビューでの詳細ページの実現)

CreateView

ListViewDetailView がデータベースからレコードを取得して情報を表示する目的で継承されるのに対し、ここから紹介するクラスはデータベースのテーブルやレコードを変更する目的で継承される Viewのサブクラス  となります。ここでいう変更とは、新規登録・更新・削除のことを言っています。

まず紹介するのが CreateView で、CreateView は特定のモデルクラスのインスタンスをレコードとしてデータベースに新規登録・新規作成することを目的に継承される Viewのサブクラス  となります。

レコードの新規登録はレコードを保存するテーブルの種類によって様々な意味合いとなり、例えばユーザーの新規登録やコメントの新規投稿などを行うこともレコードの新規登録と捉えることができます。そして、これらは CreateView を継承することで簡単に実現することが可能です。

CreateViewで実現できるページの例

ListViewDetailView と大きく異なる点は CreateView はフォームを扱うことができるという点になります(以降で紹介する Viewのサブクラス も同様です)。関数ベースビューの場合、フォームを扱う際にはリクエストのメソッドが GETPOST とで処理が切り替えられるように実装する必要がありました。正直それが面倒だったのですが、CreateView 等のフォームを扱う Viewのサブクラス を継承してビューを作成する場合は、Viewのサブクラス 内部でリクエストのメソッドに応じた処理の切り替えが行われるように作られているため、そういった処理の記述は不要となって楽にフォームを扱うことができるようになります。

CreateViewの中でメソッドに応じた処理の切り替えが行われるようになっている様子

CreateView の詳細に関しては下記ページで解説を行なっています。

CreateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】CreateViewの使い方(クラスベースビューでの新規登録ページの実現)

UpdateView

CreateView がレコードの新規登録を行う Viewのサブクラス であるのに対し、UpdaateView はレコードの更新を行う Viewのサブクラス となります。

例えばユーザーの情報の更新ページのビューなどは UpdateView を継承することで簡単に実現することができます。

UpdateViewで実現できるページの例

UpdateView の詳細に関しては下記ページで解説を行なっています。

UpdateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】UpdateViewの使い方(クラスベースビューでのレコード更新ページの実現)

スポンサーリンク

DeleteView

DeleteView はレコードの削除を行う Viewのサブクラス となります。

例えば投稿済みのコメントを消したりユーザーの退会を行うビューなどは DeketeView を継承することで簡単に実現することができます。

DeleteViewで実現できるページの例

DeleteView の詳細に関しては下記ページで解説を行なっています。

DeleteViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】DeleteViewの使い方(クラスベースビューでのレコード削除ページの実現)

FormView

FormView はフォームを扱う Viewのサブクラス となります。

ここまで紹介してきた CreateViewUpdateViewDeleteView でもフォームを扱うことができますが、これらとの違いは、FormView は基本的にデータベースのレコードの変更は行わないという点になります。もう少し違う言い方をすれば、前者の3つに関してはデータベースのレコードの変更を行うことを前提としたものであるのに対し、FormView ではその前提はありません。単にフォームを扱うことに特化した Viewのサブクラス となります。

例えば CreateView では post メソッドが定義されており、この post メソッドではフォームから送信されてきたデータをレコードとしてデータベースに新規登録する処理が行われるようになっています。それに対し、FormView でも post メソッドが定義されていますが、この post ではデータベースの操作は行われません。

したがって、基本的には FormView はデータベースのレコードの変更を伴わないようなフォームを扱う際に継承することになります。

ただし、FormView を継承するクラスで post メソッドをオーバーライドすればデータベースへの操作を行うようなことも可能です。つまり、FormView は CreateViewUpdateViewDeleteView よりも用途が広く、カスタマイズ次第で実現できるビューの幅も広くなっています。

FormView の詳細に関しては下記ページで解説を行なっています。

FormViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】FormViewの使い方(クラスベースビューで汎用的なフォームを扱う)

LoginViewLogoutView

LoginView は名前の通りログインを実現する Viewのサブクラス となります。LogoutView も名前の通り、ログアウトを実現する Viewのサブクラス となります

ログインページのビューやログアウトページのビューは、それぞれ LoginViewLogoutView を継承するで簡単に実現することができます。

LoginViewで実現できるページの例

LoginViewLogoutView の詳細に関してはそれぞれ下記ページで解説しています。

LoginViewの使い方の解説ページアイキャッチ 【Django】LoginViewの使い方(クラスベースビューでのログインの実現) LogoutViewの使い方の解説ページアイキャッチ 【Django】LogoutViewの使い方(クラスベースビューでのログアウトの実現)

スポンサーリンク

クラスベースビューのメリット

さて、今回クラスベースビューについて説明してきましたが、このクラスベースビューは今まで利用してきた関数ベースビューに比べてどのようなメリットがあるのでしょうか?

その点について解説していきたいと思います。

処理の実装量が減る

まず、このページの前半でも説明したように、関数ベースビューの場合、実現したいビューに合わせて処理を実装することでビューを作成していくことになります。それに対しクラスベースビューの場合は Django フレームワークによって提供される Viewのサブクラス を実現したいビューに合わせてカスタマイズすることで作成することになります。そして、このカスタマイズの多くはクラス変数の定義で行うことができます。

そのため、クラスベースでビューを作成することで処理の実装量が関数ベースビューに比べて減ります。例えば関数ベースビューでフォームを扱おうとするとメソッドの種類に応じて if 文で動作を切り替えるような処理を実装する必要がありますが、クラスベースビューの場合は Viewのサブクラス 側でメソッドの種類に応じた動作の切り替えが行われるようになっているため、そういった処理の実装も不要となります。

また、クラスベースビューではメソッドのオーバーライドによってカスタマイズを行うこともでき、この場合は処理の実装量が増えることになります。が、それでも関数ベースビューに比べれば、クラスベースビューの方が処理の実装量は減ります。

実装量が減りますので、その分、アプリの開発工数の削減・アプリの開発期間の短縮を行うことが可能となります。

バグが減る

そして、処理の実装量が減ることでバグも減ります

実装する処理が増えたり、処理の複雑度が上がると発生するバグの量も多くなるのが一般的です。そのため、クラスベースビューでビューを作成することによって実装必要な処理を減らしてやれば、その分バグの数も減らすことができると考えられます。そしてこれにより、ウェブアプリの品質も上がることになります。

スポンサーリンク

品質の高さも引き継げる

Viewのサブクラス は Django フレームワークから提供されるクラスであり、Django で開発された世界中のウェブアプリから利用されているクラスです。また、多くの Django ユーザーから利用されています。つまり、Viewのサブクラス は利用実績が非常に多いクラスとなります。そしてその分、品質が高いです。クラスベースビューは、その Viewのサブクラス を継承して作成していくことになるため、Viewのサブクラス の品質の高さに伴って自身で開発するクラスベースビューのビューも品質が高くなります

それに対し、関数ベースビューのビューはウェブアプリ開発者自身が作成するビューであり、そのビューを実装したウェブアプリからのみ利用されることになります。それだけで品質が低いとは言い切れませんが、それでも品質を高めるためにはウェブアプリ開発者や評価者がしっかりテストを行う必要があります。

もちろん、クラスベースビューの場合もカスタマイズ部分がバグる可能性もありますが、それでも実装する処理の量は減るので、前述の通り関数ベースビューに比べてバグの数は減り、実装量が少ない分バグを見つけやすくもなると思います。

コードの重複が減る

また、関数ベースビューの場合、複数の関数に同じような処理の実装が必要となることが多いです。例えばコメント一覧の表示を行うビューの関数とユーザー一覧の表示を行うビューの関数では、扱うモデルクラスが異なるものの、実装する処理は大体同じになります。例えば下記のような感じになると思います。

コメント一覧とユーザー一覧の表示
from django.shortcuts import render
from .models import Comment, User

def comments_view(request):
    comments = Comment.objects.all()

    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)


def users_view(request):
    users = User.objects.all()

    context = {
        'users' : users
    }

    return render(request, 'forum/users.html', context)

同じような処理を実装すれば良いだけなので開発時はそんなに苦にならないかもしれません。ほぼコピペで済みますので…。ですが、こういった同じような処理が複数箇所に存在すると、その処理の中にバグがあると全箇所を修正する必要が出てきます。同じような処理が多ければ多いほど修正が大変になりますし、修正漏れも発生しやすくなります。

なので、こういった同じような処理は関数化するなどして実装箇所が一箇所にまとまるように工夫する必要があります。

ですが、工夫しなくても、こういったことを自然と実現できるのがクラスベースビューとなります。

前述のような関数ベースビューで実装される典型的な処理・重複しがちな処理は Viewのサブクラス で既に実装されています。例えば上記で示したようなインスタンスの一覧を表示する例であれば ListView で実装されています。したがって、Viewのサブクラス を継承して実現したいビューに合わせてカスタマイズしてやることで、自然と実装する処理が重複することを防ぐことができるようになっています。

そして、これによって修正が容易となり、保守性が向上することになります。

クラスベースビューのデメリット

続いて、関数ベースビューと比較した時のクラスベースビューのデメリットについて説明します。

スポンサーリンク

Viewのサブクラス の知識が必要

まず、クラスベースビューを使いこなすためには Viewのサブクラス の知識が必要になります。

クラス変数として何を定義すればクラスの動作がどう変化するのか?

メソッドとして何を定義すればクラスの動作がどう変化するのか?

この辺りの知識がないとクラスベースビューを使いこなすのは難しいと思います。関数ベースビューの場合は、リクエストを受け取り、レスポンスを返却することさえ満たせば、あとは自由に実装して好きなビューを作成することができます。知識よりも実装力の方が重要となります。ですが、クラスベースビューの場合は継承する Viewのサブクラス の知識が必要で、これがないと自由自在にビューを作るようなことができません。

作っている感が減る?

また、ウェブアプリを “作っている感” に関しては関数ベースビューの方が上かなぁと思います。個人的には関数ベースビューの方がビューを作っていて楽しいですね…。自由に好きなように実装し、自身が意図した通りにビューが動作してくれた時はやはり嬉しいです。

ですが、クラスベースビューの場合は単にクラス変数の定義だけでビューが実現できてしまうので逆に物足りなさを少し感じたりもします。まぁ、この辺りは好みかもしれないですね!

もちろん、お試しでウェブアプリを開発する場合などは、関数ベースビューでもクラスベースビューでもどちらでも良いと思いますが、最終的な実装としてはクラスベースビューの方が良いと思います。これは、先ほどメリットで挙げたようにクラスベースビューの方が品質・保守性などが関数ベースビューに比べて高くなるからです。

ということで、最初は関数ベースビューでウェブアプリを開発するのでも良いのですが、最終的にはクラスベースビューも使いこなせるようになっておいた方が良いと思います。

掲示板アプリのビューをクラスベースに変更する

最後に、掲示板アプリのビューをクラスベースに変更する例を示していきたいと思います。

この Django 入門の連載の中では簡単な掲示板アプリを開発してきており、前回の連載では下記ページで掲示板アプリで発生していた N + 1 問題の解決を行ないました。

DjangoにおけるN+1問題の解説ページアイキャッチ 【Django入門13】N+1問題とselect_related・prefetch_relatedでの解決

今回は、上記ページの 掲示板アプリの N + 1 問題を解決する で示した views.py の全関数をクラスベースビューに置き換えていきたいと思います。変更対象となるファイルは views.pyurls.py (アプリ側) のみとなります。

クラス変数のデフォルト設定 でも説明したように、各 Viewのサブクラス のデフォルト設定に合わせてテンプレートファイルなどの名前を変更してウェブアプリを開発していくやり方もあるのですが、今回は、既に用意しているテンプレートファイル等に合わせて Viewのサブクラス を継承するクラスをカスタマイズしていきたいと思います。

また、念の為に示しておくと、現状の views.py は下記のようになっています。1つ1つクラスベースのビューに置き換えていきましょう!

変更前のviews.py
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, logout
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator

User = get_user_model()


def login_view(request):
    if request.method == 'POST':
        form = LoginForm(data=request.POST)

        if form.is_valid():
            user = form.get_user()

            if user:
                login(request, user)
                return redirect('user', user.id)

    else:
        form = LoginForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/login.html', context)


def logout_view(request):
    logout(request)
    return redirect('login')


def index_view(request):
    return redirect(to='comments')


@login_required
def users_view(request):
    users = User.objects.prefetch_related('comments').all()

    paginator = Paginator(users, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj': page_obj
    }

    return render(request, 'forum/users.html', context)


@login_required
def user_view(request, user_id):
    user = get_object_or_404(User, id=user_id)
    comments = Comment.objects.filter(user=user)

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'user': user,
        'page_obj': page_obj
    }

    return render(request, 'forum/user.html', context)


@login_required
def comments_view(request):
    comments = Comment.objects.select_related('user').all()

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj': page_obj
    }

    return render(request, 'forum/comments.html', context)


@login_required
def comment_view(request, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)

    context = {
        'comment': comment
    }

    return render(request, 'forum/comment.html', context)


def register_view(request):

    if request.method == 'POST':

        form = RegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)

            return redirect('user', user.id)

    else:
        form = RegisterForm()

    context = {
        'form': form
    }

    return render(request, 'forum/register.html', context)


@login_required
def post_view(request):
    if request.method == 'POST':
        form = PostForm(request.POST)

        if form.is_valid():
            comment = form.instance
            comment.user = request.user
            comment.save()

            return redirect('comments')
    else:
        form = PostForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/post.html', context)

スポンサーリンク

login_view から LoginView のサブクラスへの置き換え

まずは、login_view をクラスベースビューに置き換えていきたいと思います。ログインを実現する Viewのサブクラス として LoginView が用意されていますので、この LoginView を継承するクラスを定義し、そのカスタマイズを行うことで login_view 同等のビューを実現していきます。

最初なので、このクラスベースビューへの置き換えについて少し詳しく説明していきたいと思います。

まず、login_view では、メソッドが GET のリクエストを受け取った際に LoginForm のクラスのインスタンスをコンテキストの 'form' キーにセットし、 'forum/login.html' のパスにあるテンプレートファイルから HTML を生成してレスポンスとして返却するようになっています。また、メソッドが POST のリクエストを受け取った際には送信されてきたデータに基づいて認証を行い、認証 OK の場合に ③ログインしたユーザーの詳細ページの URL にリダイレクトするようになっています(ユーザーの詳細ページとは /forum/comment/ユーザーID/ の URL のページとなります)。

それに対し、LoginView では、大雑把に説明すれば、メソッドが GET のリクエストを受け取った際に①クラス変数 form_class で指定されるクラスのインスタンスをコンテキストの 'form' キーにセットし、②クラス変数 template_name で指定されるパスにあるテンプレートファイルから HTML を生成してレスポンスとして返却するようになっています。また、メソッドが POST のリクエストを受け取った際には送信されてきたデータに基づいて認証を行い、認証 OK の場合に③クラス変数 success_url で指定される URL or  メソッド get_success_url から返却される URL にリダイレクトするようになっています。

この LoginView の処理の流れで示した①②③の部分を login_view の処理の流れで示した①②③の部分と同等のものになるようにカスタマイズしてやれば、LoginView を継承するクラスで login_view と同等の処理を実現することができるようになります。

まず①と②に関しては、単に LoginView を継承するクラスでクラス変数を下記のように定義してやれば良いことになります。

  • form_class = LoginForm
  • template_name = 'forum/login.html'

ちょっと厄介なのが③です。login_view においては redirect('user', user.id) によってログインしたユーザーの詳細ページへのリダイレクトが実現されていますが、ログインしたユーザーは実際にアプリ動作中にユーザーがログインを行わないとわかりません。したがって、リダイレクト先の URL はアプリ起動時には分かりません。つまり、この URL をクラス変数の定義時に指定することは不可能です。

そのため、実際にログイン操作が行われた際に、ログインしてきたユーザーに合わせて動的にリダイレクト先の URL を指定してやる必要があります。で、動的に処理を変化させるためにオーバーライドを利用する で説明したように、URL 等のパラメーターを動的に変化させるためにはメソッドのオーバーライドが必要になります。今回の場合は、LoginView の持つ get_success_url がオーバーライドの対象であり、具体的には下記のようにメソッドを定義することで、ログインしたユーザーの詳細ページの URL を返却することができるようになります。そして、この返却値に対してリダイレクトが行われることになります。

get_success_urlのオーバーライド
def get_success_url(self):
    return reverse('user', kwargs={'user_id':self.request.user.id})

get_success_url で返却すべきデータは URL であるという点に注意してください。login_view はビューであるため、必ずレスポンスを返却する必要があります。そのため、redirect 関数でレスポンスを生成し、それを return で返却するようになっています。ですが、get_success_url は URL を返却する必要があるため、redirect 関数の実行結果を返却すると get_success_url の呼び出し元が期待しているデータが返却されないことになります。こういった違いも考慮しながら、関数ベースビューとクラスベースビューを使いこなしていく必要があります。

ということで、login_viewLoginView に置き換えた結果は下記のようになります。import 部分は省略していますが、後ほど import 部分を含めた views.py 全体のソースコードを紹介しますので、必要な import 処理に関してはそこで確認していただければと思います。

Login
class Login(LoginView):
    form_class = LoginForm
    template_name = 'forum/login.html'

    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.request.user.id})

最初の例としてはちょっと難しかったかもしれませんが、基本的にはここで説明したように、関数ベースビューで行っている処理を整理し、それをクラスベースビューで実現できるようにクラス変数の定義やメソッドのオーバーライドを行なっていくことがクラスベースビューへの置き換えを実現する際の基本的な流れになります。また、実際に関数ベースビューをクラスベースビューに置き換えることでクラスベースビューの仕組みも理解しやすくなると思いますので、是非以降の解説も読み進めていただきたいですし、自身で定義した関数ベースビューをクラスベースビューに置き換えることにも挑戦してみていただきたいです。

logout_view から LogoutView のサブクラスへの置き換え

続いて logout_view をクラスベースに置き換えていきます。Django フレームワークにはログアウトを実現する LogoutView が用意されており、この LogoutView を利用すればログアウトを行うビューも簡単に実現可能です(そもそも関数ベースでも簡単ですが…)。

logout_view で行っているのはログアウト処理とログインページの URL へのリダイレクトになります。また、LogoutView は動作した際にログアウト処理とクラス変数 next_page へのリダイレクトが行われるようになっています。なので、logout_view と同等の LogoutView を継承するクラスは下記を定義することで実現することができます。

Logout
class Logout(LogoutView):
    next_page = reverse_lazy('login')

reverse_lazy によって、名前 'login' から URL への変換を行い、その変換結果を next_page に指定するようにしています。urls.py でログインページの URL に 'login' という名前をつけるようにしていますので、上記のように next_page を定義しておけば、ログアウト後にログインページにリダイレクトされることになります。

reverse_lazy の詳細は下記ページで紹介しているので、詳しくはこちらをご参照ください。クラスベースビューのクラス変数の定義時には、reverse ではなく reverse_lazy を利用する必要がある点がポイントとなります。

reverseとreverse_lazyの違いの解説ページアイキャッチ 【Django】reverseとreverse_lazyの違い

index_view から RedirectView のサブクラスへの置き換え

次の index_view に関しても簡単で、index_view では単純に、名前が 'comments' に設定された URL へのリダイレクトを行なっているだけになります。

View のサブクラス としても単にリダイレクトを行うだけのクラスが用意されており、それが RedirectView となります。

RedirectView は動作する際にクラス変数 url で指定される URL にリダイレクトを行うようになっているため、クラス変数 url を定義し、名前が 'comments' に設定された URL を指定してやれば index_view と同等の動作を実現することができます。その例が下記となります。

Index
class Index(RedirectView):
    url = reverse_lazy('comments')

スポンサーリンク

users_view から ListView のサブクラスへの置き換え

次は users_view のクラスベースビューへの置き換えを行なっていきます。

users_viewUser (CustomUser) のインスタンスの一覧を表示するビューであり、こういったインスタンスの一覧を実現する View のサブクラスListView となります。

users_view をクラスベースビューに置き換える上でポイントになるのは下記の4つになると思います。

  • users_view では N + 1 問題の対策としてデータベースに発行するクエリを prefetch_related を利用して生成している
  • users_view ではページネーションが行われている
  • users_view からは 'forum/users.html' のテンプレートファイルが利用されている
  • users_view では @login_required によって非ログインユーザーからのアクセスを禁止している

users_view をクラスベースビューに置き換えるという観点で重要になるのは、これらの users_view で行なっていることがクラスベースのビューでも行われるようにする必要があるという点になります。

クエリの指定

まず1つ目のインスタンスを取得するために発行するクエリの設定はクラス変数 queryset の定義で実現することができます。users_view の時に発行されていたクエリと同じものが設定されるように、下記のように queryset クラス変数を定義すれば良いです。

クエリ関連のクラス変数
queryset = User.objects.prefetch_related('comments').all()

ページネーションの設定

また、2つ目のページネーションに関して説明すると、ページネーション自体はクラス変数 paginate_by を定義することで実現することができます。クラス変数 paginate_by を定義して整数を指定しておくことで、各ページに指定した整数分のインスタンスが自動的に割り付けられるようになります。そして、各ページの Page のインスタンスはテンプレートファイルから page_obj という変数名で参照することが可能となります。

また、users_view では表示するページのページ番号をクエリパラメーター 'p' から取得するようになっています。ここも同様のものとするのであれば page_kwarg の定義も必要となります。具体的には、page_kwarg = 'p' を定義しておくことで、users_view と同様にページ番号をクエリパラメーター 'p' から取得するようになります。

そのため、users_view と同様のページネーションを実現するためには、下記のように paginate_bypage_kwarg のクラス変数の定義が必要となります。

ページネーション関連のクラス変数
paginate_by = 3
page_kwarg = 'p'

テンプレートファイルの指定

また、users_view ではテンプレートファイル 'forum/users.html' を利用するようになっているため、クラスベースのビューからも、このファイルを利用するようにカスタマイズを行う必要があります。

これは、利用するテンプレートファイルに合わせてクラス変数 template_name の定義を行えば良いだけになります。users_viewtempalates フォルダからの相対パスで 'forum/users.html' の位置にあるファイルをテンプレートファイルとして利用しているわけですから、下記のように template_name を定義してやれば良いことになります。

テンプレートファイル関連のクラス変数
template_name = 'forum/users.html'

非ログインユーザーのアクセス禁止

また、非ログインユーザーのアクセスからのアクセスの禁止は、クラスベースビューの場合は @login_required の指定ではなく LoginRequiredMixin の継承によって実現することができます。このように、クラス変数やメソッドのオーバーライドだけでなく、ミックスインの継承によってもビューをカスタマイズすることができることは覚えておくと良いと思います。

ということで、users_view と同等のクラスベースビューは、下記のような UserList によって実現することができることになります。

UserList
class UserList(LoginRequiredMixin, ListView):
    queryset = User.objects.prefetch_related('comments').all()
    template_name = 'forum/users.html'
    paginate_by = 3
    page_kwarg = 'p'

ListView の詳細や、ここで定義したクラス変数に関しては下記ページで解説していますので、詳しくは下記ページを参考にしていただければと思います。

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現)

ついでなので、ここでクラス変数 template_name に指定している 'forum/users.html' と上記の UserList との関連性について確認しておきたいと思います。forum/users.html は下記のようなテンプレートファイルとなっています。ポイントは、このテンプレートファイルから参照している変数になります。

forum/users.html
{% extends "forum/base.html" %}

{% block title %}
ユーザー一覧
{% endblock %}

{% block main %}
<h2>ユーザー一覧(全{{ page_obj.paginator.count }}人)</h2>
<table class="table table-hover">
   
    <thead>
        <tr>
            <th>ユーザー</th><th>コメント数</th>
        </tr>
    </thead>
    <tbody>
        {% for user in page_obj %}
        <tr>
            <td><a href="{% url 'user' user.id %}">{{ user.username }}</a></td>
            <td>{{ user.comments.all|length }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

この forum/users.html からは page_obj という変数を参照しています。この page_objPage クラスのインスタンスであることを期待しており、この page_obj のデータ属性を出力することで前後のページ等へのリンクを設定し、さらに page_obj からインスタンス user を取得し、この user のデータ属性を出力することで各ユーザーの情報の表示を実現するようになっています。

また、テンプレートファイルから HTML を生成する際には、テンプレートファイルから参照する変数がコンテキストにセットされている必要があります。forum/users.html の場合は、'page_obj' という変数名で変数を参照しており、この変数は Page のインスタンスである必要があるため、コンテキストの 'page_obj' キーの値として Page のインスタンスがセットされている必要があります。

実際、元々このテンプレートファイルを利用していた users_view では下記のような処理を行なっており、これにより上記のようなコンテキストの作成を行なっています。

users_viewのコンテキスト生成
page_obj = paginator.page(number)

context = {
    'page_obj': page_obj
}

では、先ほど示した UserList のような ListView を継承するクラスでは、上記のようにコンテキストを生成する処理を実装する必要はないのでしょうか?

結論としてはケースバイケースとなります。ですが、少なくとも今回の例で扱っている users_view と同等の動作を実現するだけであればコンテキストを生成する処理の実装は不要です。なぜなら ListView が、ページネーションを行う際には、表示するページに対する Page のインスタンスがコンテキストの 'page_obj' キーの値としてセットされるように実装されているからになります。つまり、上記で示した users_view と同様のコンテキスト生成が行われるように ListView が実装されています。なので、UserList の場合も forum/users.html からは page_obj が参照できるようになっています。

ただし、テンプレートファイルから page_object などのような page_obj 以外の変数名で Page のインスタンスを参照している場合は話は別で、この場合は Page のインスタンスをコンテキストの 'page_object' キーの値としてセットする必要があることになります。それに対し、前述の通り ListView では Page のインスタンスは 'page_obj' キーの値としてセットすることになっているため、この場合はテンプレートファイルから Page のインスタンスの参照ができなくなります。したがって、この場合は、テンプレートファイルから Page のインスタンスを参照できるようにするため、コンテキストのキー名等を変更するようなカスタマイズが必要となります。

このように、Django でウェブアプリを開発する際は、テンプレートファイルが参照する変数に合わせて適切なコンテキストを生成することが重要となります。これは関数ベースビューの場合も言えることではあるのですが、関数ベースビューの場合はコンテキストを生成する処理も開発者自身が実装することになるため、開発者が好きなようにコンテキストにデータをセットするキーやテンプレートファイルから参照する変数を決めることができます。それに対し、クラスベースビューの場合、View のサブクラス の中で自動的にコンテキストが生成されるため、そのコンテキストに合わせてテンプレートファイルを実装する必要があります。

なので、View のサブクラス の中で生成されるコンテキストがどのようなものであるのかも知っておいた方が開発はしやすくなります。この点についても、View のサブクラスの種類 で紹介するリンク先の各ページで紹介していますので、是非こちらも参考にしてください。

user_view から DetailView のサブクラスへの置き換え

次は user_view のクラスベースビューへの置き換えを行なっていきます。

user_viewUser (CustomUser) の1つのインスタンスの詳細を表示するビューであり、こういった1つのインスタンスの詳細の表示を実現する View のサブクラスDetailView となります。

この DetailView の詳細に関しては下記ページで解説しています。

DjangoのDetailViewの解説ページアイキャッチ 【Django】DetailViewの使い方(クラスベースビューでの詳細ページの実現)

DetailView を継承すれば、1つのインスタンスの情報の詳細を表示するビュー自体は簡単に実現することは可能なのですが、user_view と同等のビューを実現するのは結構難しいです。なぜなら、user_view では下の図のように User の1つのインスタンスのみではなく、そのインスタンスとリレーションが構築されている Comment のインスタンスの一覧も表示するようになっているからです。要は、特定のユーザーの詳細情報と、そのユーザーが投稿したコメントの一覧が表示されるようになっています。しかもコメントの一覧はページネーションによってページ分割された状態で表示されるようになっています。

user_viewで表示されるページ

なので、ビューでは特定の User のインスタンスのみではなく、Comment のインスタンスの集合もコンテキストにセットしてテンプレートに渡す必要があります。それに対し、DetailView では特定の1つのインスタンスのみをコンテキストにセットするようになっています。そのため、特定の User のインスタンスのみをコンテキストにセットし、このインスタンスの情報のみを表示することは容易に実現することができますが、それだけだと上図のようなページ表示が実現できません。上図のようなページ表示を実現するためには、Comment のインスタンスの集合もコンテキストにセットできるようにカスタマイズを行う必要があります。この辺りが、user_view のクラスベースビューへの置き換えを行う上でのポイントになると思います。

特定の User のインスタンスのみの表示

まずは特定の User のインスタンスの詳細情報の表示のみを実現するビューを作成し、その次に Comment のインスタンスの一覧を表示するようビューを変更するという流れで、段階的に解説していきたいと思います。

特定の User のインスタンスの詳細情報の表示のみを実現するビューは下記の UserDetail によって実現することが可能です。LoginRequiredMixin を継承しているのは、前述の通り、非ログインユーザーからのアクセスを禁止するためです。

UserDetail(Userのみ)
class UserDetail(LoginRequiredMixin, DetailView):
    model = User
    pk_url_kwarg = 'user_id'
    template_name = 'forum/user.html'

まず、クラス変数 model によってインスタンス(レコード)の取得先のモデルクラス(テーブル)の指定を行なっています。 model = User と定義しているため、 UserDetail が動作する際には User (より正確には CustomUser) のテーブルから1つのインスタンスが取得されることになります。

また、DetailView は、その取得するインスタンスを特定するための条件が URL で指定されることを前提とした作りとなっています。これは関数ベースビューの場合も同様で、user_view に対するurlpatterns の要素は下記のように指定されており、URL における user/<int:user_id>/<int:user_id> 部分に指定された整数が、user_view 関数に引数 user_id として渡されるようになっています。そのため、user_view では引数 user_id をインスタンスを特定するための情報として利用し、 1つのインスタンスのみを取得することができるようになっています。

関数ベースビューの時のurls.py
from django.urls import path
from . import views

urlpatterns = [
    # 略
    path('user/<int:user_id>/', views.user_view, name='user'),
    # 略
]

DetailView の場合も同様で、上記と同等の urlpatterns を定義した場合、<int:user_id> 部分に指定された整数が DetailView に引数 user_id として渡されるようになっています。ですが、引数受け取り側の DetailView では、インスタンスを特定するための整数を引数 pk として受け取ることを前提とした作りになっています。つまり、urlpatternsDetailView とで話が合っていないことになります。この場合、DetailViewが動作した際に例外が発生することになります。

プライマリーキーを渡す引数がビューとビュー以外とで話があっていない様子

ただし、インスタンスを特定するための整数を受け取る引数の引数名は pk から変更可能です。この引数名を変更するために定義するクラス変数が pk_url_kwarg で、このクラス変数を定義することで、この引数名を pk から変更することが可能です。UserDetail では pk_url_kwarg = 'user_id' を定義しているため、この引数名が user_id となり、URL で <int:user_id> 部分に指定された整数を UserDetail が受け取ることができるようになります。

プライマリーキーを渡す引数がビューとビュー以外とで話が合っている様子

そして、UserDetail は、受け取った整数とプライマリーキーが一致するインスタンスを User のテーブルから取得することになります。

解説を読んで気づかれた方もおられるかもしれませんが、わざわざ pk_url_kwarg を定義しなくても、urlpatterns の中で実行している path 関数の第1引数における <int:user_id> 部分を <int:pk> に変更するのでもオーケーです。大事なのは、ビューとビュー以外の部分とで話が合うように、それぞれのファイルを実装することです。

あとは、template_nameuser_view から利用しているテンプレートファイルに合わせて定義してやれば、特定のユーザーの詳細情報を表示するページは完成することになります。

Comment の一覧の追加

次は、取得したインスタンスとリレーションが構築されている Comment のインスタンスの一覧の表示を行うようにしていきたいと思います。最初に結論を言えば、下記のように UserDetailget_context_data メソッドの定義を追加することで、この Comment のインスタンスの一覧の表示を実現することが可能です。

UserDetail
class UserDetail(LoginRequiredMixin, DetailView):
    model = User
    pk_url_kwarg = 'user_id'
    template_name = 'forum/user.html'

    def get_context_data(self, **kwargs):
        comments = Comment.objects.filter(user=self.object)

        paginator = Paginator(comments, 3)
        number = int(self.request.GET.get('p', 1))
        page_obj = paginator.page(number)

        return super().get_context_data(page_obj=page_obj)

ここで定義した get_context_dataDetailView の持つメソッドで、コンテキストを生成して返却するメソッドとなっています。DetailViewget_context_data では可変個のキーワード引数を受け取れるようになっており、下記のように各キーにデータをセットしたコンテキストを生成するようになっています。

  • 'object': model のインスタンス
  • 'modelに指定したモデルクラスの名前(小文字)': model のインスタンス
  • 'キーワード引数 1 の引数名': キーワード引数 1 の値
  • 'キーワード引数 2 の引数名': キーワード引数 2 の値
  • (以下、同様にして指定されたキーワード引数に応じてデータがセットされる)

ただし、この get_context_dataDetailView で定義される get メソッドから実行されるようになっており、その際にはキーワード引数が指定されないようになっています。したがって、コンテキストにセットされるデータは model に指定したモデルクラスのインスタンスのみとなります。なので、UserDetail の場合は User のインスタンスしかテンプレートファイルから参照できないことになります。これだとテンプレートファイルからは Comment のインスタンスが参照できません…。

ですが、前述の通り get_context_data 自体は可変個のキーワード引数を受け取れるようになっており、そこで指定した引数に応じてコンテキストにデータがセットされるようになっています。そのため、get_context_data をオーバーライドし、コンテキストにセットしたいデータを用意してキーワード引数を指定した状態でスーパークラス(DetailView)のメソッドを実行するようにすれば、任意のデータをコンテキストにセットできることになります。

それを行なっているのが上記の UserDetailget_context_data で、UserDetailget_context_data では、事前に model から取得されたインスタンスとリレーションが構築されているインスタンスの集合を取得し、そのインスタンスの集合からページネーションを行い、クエリパラメーター 'p' で取得されるページ番号の Page のインスタンス page_obj を生成するようになっています(事前に取得された model のインスタンスは self.object にセットされています)。

さらに、その生成した page_obj をキーワード引数に指定する形でスーパークラス、つまり DetailViewget_context_data を実行してコンテキストの生成を行なっています。この際、キーワード引数で page_obj=page_obj を指定しているため、下記のように各キーにデータがセットされたコンテキストが生成されることになります。

  • 'object': User のインスタンス
  • 'user': User のインスタンス
  • 'page_obj': page_obj (Page のインスタンス)

そのため、テンプレートファイルからは object or user を参照して、その User の詳細情報を表示することもできますし、page_obj を参照し、そのページに割り付けられた Comment の集合の各インスタンスの情報を表示するとこも可能となります。また、page_obj を参照することで、前後のページへのリンクを設定するようなことも可能です。コードの掲載は省略しますが、forum/user.html では userpage_obj を参照するようになっているため、上記のようにコンテキストが生成されれば、テンプレートファイルの変更無しに user_view の時と同様のページの表示が行えることになります。ただし、テンプレートファイルで別の変数を参照している場合は、生成するコンテキスト or テンプレートファイルが参照する変数の変数名を変更する必要があるので注意してください。

上記はちょっと無理矢理な例になるかもしれませんが、メソッドのオーバーライドを利用すれば様々なページ表示が実現できることは理解していただけたのではないかと思います。

また、下記ページの extra_context の節でも説明していますが、コンテキストへの要素の追加はクラス変数 extra_context の定義によっても実現可能です。下記は ListView の解説ページとなりますが DetailView でもクラス変数 extra_context の定義によってコンテキストへの要素の追加が可能です。

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現)

ただし、今回はコンテキストにセットする Page のインスタンスを動的に変化させる必要があったため、もっと具体的に言えばクエリパラメーター p で指定されるページ番号に応じて Page のインスタンスを動的に変化させる必要があったため、get_context_data のオーバーライドによってコンテキストに追加する要素を動的に設定するようにしています。

comments_view から ListView のサブクラスへの置き換え

comments_view のクラスビューへの置き換えに関しては users_view から ListView のサブクラスへの置き換え とほぼ同様であるため、解説は省略します。

comments_view と同等のクラスベースビューは下記の CommentList によって実現することができます。

CommentList
class CommentList(LoginRequiredMixin, ListView):
    queryset = Comment.objects.select_related('user').all()
    paginate_by = 3
    page_kwarg = 'p'
    template_name = 'forum/comments.html'

スポンサーリンク

comment_view から DetailView のサブクラスへの置き換え

comment_view のクラスビューへの置き換えに関しても user_view から DetailView のサブクラスへの置き換え とほぼ同様なので、この解説に関しても省略します。ただ、comment_view の場合は Comment のインスタンスの情報さえ表示されば良いようになっているため、get_context_data のオーバーライドは不要となります。

comment_view と同等のクラスベースビューは下記の CommentDetail によって実現することができます。

CommentDetail
class CommentDetail(LoginRequiredMixin, DetailView):
    model = Comment
    pk_url_kwarg = 'comment_id'
    template_name = 'forum/comment.html'

register_view から CreateView のサブクラスへの置き換え

続いて register_view のクラスベースビューへの置き換えを行なっていきます。

register_view はユーザー登録を行うためのビューになっています。このユーザー登録は User (CustomUser) のインスタンスのデータベースへの新規登録によって実現することができ、こういったインスタンスの新規登録を実現する View のサブクラスCreateView となります。

form_class の定義

こういったインスタンスの新規登録を行う際には、利用者から新規登録するインスタンスの情報をフォームで入力してもらうことが多いです。そして、そのフォームから送信されたデータに従ってインスタンスのデータベースへの新規登録を行います。

そのため、CreateView を継承するクラスではクラス変数 form_class を定義し、このクラス変数で利用するモデルフォームを指定する必要があります。モデルフォームに関しては下記ページで解説しているため、詳しくは下記ページを参照してください。

Djangoのモデルフォーム解説ページアイキャッチ 【Django入門8】モデルフォームの基本

register_view ではモデルフォームとして RegisterForm を利用しているため、form_class = RegisterForm を定義してやれば良いことになります。RegisterFormUser (CustomUser) をベースとするモデルフォームとなります。

この form_class の定義により、CreateView を継承するクラスがメソッド GET のリクエストを受け取った際には form_class で指定されたモデルフォームに基づいたフォームがページに表示されることになります。さらに、メソッド POST のリクエストによってフォームからデータが送信されてきた際には、まず送信されてきたデータの妥当性の検証が行われます。検証結果 OK の場合は、送信されてきたデータに基づいて User のインスタンスが生成され、それがデータベースに新規作成されることになります。そして、データベースへの新規作成後に、特定の URL へのリダイレクトが行われます。この辺りの一連の処理の流れは CreateView で実装されているため、CreateView を継承することで、このような一連の処理の流れも簡単に実現することができるようになります。

form_valid メソッドの定義

ただし、register_view では妥当性の検証結果が OK の場合に User のインスタンスのデータベースへの新規登録を行うだけでなく、この新規登録を行なった後に、その User のインスタンスに対するログイン処理が実施されることになっています。

ですが、CreateView ではデータベースへのインスタンスの新規登録が行われるだけで、ログイン処理は実施されません。こういった、CreateView に実装されていない処理を追加で行わせたいような場合はクラスのカスタマイズが必要になります。

今回のログイン処理の場合は、form_valid メソッドのオーバーライドにより実現することができます。CreateViewform_valid は、フォームから送信されてきたデータに対する妥当性の検証結果が OK の場合に実行されるメソッドであり、インスタンスの保存(新規登録)および、クラス変数 success_url で指定される URL  or get_success_url で返却される URL へのリダイレクトのレスポンスの返却のみを行うようになっています。

なので、form_valid をオーバーライドし、これらの処理に追加してログイン処理を実施するようにしてやれば良いことになります。具体的には、下記のような form_valid でオーバーライドを行えば良いです。

form_valid
def form_valid(self, form):
    response = super().form_valid(form)
    user = self.object
    login(self.request, user)
    return response

1行目でスーパークラス(CreateView)の form_valid が実行され、インスタンスがデータベースにレコードとして保存され、さらにリダイレクトのレスポンスが返却値として返却されます。

さらに、2行目で、1行目で保存されたインスタンスを取得しています。CreateView を継承するクラスの場合、保存されたインスタンスは self.object から取得可能で、form_class で指定したモデルフォームのベースとなっているモデルクラスのインスタンスが取得されます。form_class = RegisterForm を定義しておけば、User のインスタンスが取得されることになります。

そして、3行目で、その User のインスタンスを引数に指定してログイン処理を実施し、最後にスーパークラスの form_valid の返却値を返却することで処理を終了しています。

このように form_valid のオーバーライドを行うことで、CreateViewform_valid で行われる処理だけでなく、他の処理も form_valid 実行時に行われるようにすることができます。

ということで、register_view 同等のクラスベースビューは下記の Register によって実現することができます。詳細な説明は省略させていただいていますが、register_view でも login_view 同様に、ログイン後にログインユーザーの詳細情報を表示するページへのリダイレクトが行われるようになっていますので、login_view から LoginView のサブクラスへの置き換え で説明した内容と同様に、そのリダイレクト先の URL の取得を get_success_url のオーバーライドによって実現しています。

Register
class Register(CreateView):
    model = User
    form_class = RegisterForm
    template_name = 'forum/register.html'

    def form_valid(self, form):
        
        response = super().form_valid(form)
        user = self.object
        login(self.request, user)
        return response
    
    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.object.pk})

post_view から CreateView のサブクラスへの置き換え

ビューの変更の最後に post_view のクラスベースビューへの置き換えを行なっていきます。

post_view ではフォームから送信されてきたデータに応じた Comment のインスタンスのデータベースへの新規登録が行われるようになっています。新規登録を行うわけですから、先ほど紹介した Register 同様に CreateView を継承してクラスを定義することで、この post_view と同等のクラスベースビューも実現することができます。なので、基本的には register_view から CreateView のサブクラスへの置き換え を参考にしてクラスを定義していけば良いことになります。

ですが、post_view の場合、Comment のインスタンスをデータベースに保存する前に、そのインスタンスとログインユーザーの User のインスタンスとの間にリレーションを構築する必要があります。要は、コメントの投稿者が誰であるかを設定した後に、データベースへの保存を行う必要があります。CreateView では、基本的にフォームから送信されてきたデータに基づいてインスタンスの生成が行われるため、こういったフォームから送信されてこないデータの設定等は form_valid のオーバーライドによって実現する必要があります(インスタンスの新規登録時の日付の設定などは自動的に行われるようにモデルクラスを定義するようなこともできます)。

具体的には、下記のような form_validCreateView で定義してやれば、データベースに保存しようとしている Comment のインスタンスとログイン中ユーザーのインスタンス(self.request.user)との間にリレーションを構築したのちに、その Comment のインスタンスがデータベースに新規登録されるような動作を実現することができます。

form_valid
def form_valid(self, form):
    form.instance.user = self.request.user
    return super().form_valid(form)

また、post_view の場合、Comment のインスタンスのデータベースへの新規登録が完了した後にコメントの一覧を表示するページへのリダイレクトが行われるようになっています。投稿したコメントの詳細情報を表示するページへのリダイレクトではなく、毎回同じページへのリダイレクトが行われるわけですから、URL は静的に決まることになり、リダイレクト先の URL はアプリ起動時に設定可能です。そのため、get_success_url メソッドのオーバーライドではなく、クラス変数 success_url の定義によって、リダレクト先の URL の設定を行うことができます。

この辺りを踏まえると、post_view 同等のクラスベースビューは下記の Post によって実現することができます。

Post
class Post(LoginRequiredMixin, CreateView):
    form_class = PostForm
    template_name = 'forum/post.html'
    success_url = reverse_lazy('comments')

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

クラス変数 success_url には reverse_lazy の実行結果として得られる URL が指定されるようにしています。以前にも紹介しましたが、reverse_lazy に関しては下記ページで解説していますので、reverse_lazy について知りたい方は下記ページをご参照ください。

reverseとreverse_lazyの違いの解説ページアイキャッチ 【Django】reverseとreverse_lazyの違い

スポンサーリンク

ビューの変更点まとめ

以上で、views.py が全てクラスベースビューに置き換わったことになります。

ここまで個別に各クラスを紹介してきたため、最後に import 部分等も含めて変更後の views.py 全体を示しておきます。

views.py
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth import login
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator
from django.views.generic import ListView, DetailView, RedirectView, CreateView
from django.contrib.auth.views import LoginView, LogoutView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse, reverse_lazy

User = get_user_model()

class Login(LoginView):
    form_class = LoginForm
    template_name = 'forum/login.html'

    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.request.user.id})

class Logout(LogoutView):
    next_page = reverse_lazy('login')

class Index(RedirectView):
    url = reverse_lazy('comments')

class UserList(LoginRequiredMixin, ListView):
    queryset = User.objects.prefetch_related('comments').all()
    template_name = 'forum/users.html'
    paginate_by = 3
    page_kwarg = 'p'

class UserDetail(LoginRequiredMixin, DetailView):
    model = User
    pk_url_kwarg = 'user_id'
    template_name = 'forum/user.html'

    def get_context_data(self, **kwargs):
        comments = Comment.objects.filter(user=self.object)

        paginator = Paginator(comments, 3)
        number = int(self.request.GET.get('p', 1))
        page_obj = paginator.page(number)

        return super().get_context_data(page_obj=page_obj)


class CommentList(LoginRequiredMixin, ListView):
    queryset = Comment.objects.select_related('user').all()
    paginate_by = 3
    page_kwarg = 'p'
    template_name = 'forum/comments.html'

class CommentDetail(LoginRequiredMixin, DetailView):
    model = Comment
    pk_url_kwarg = 'comment_id'
    template_name = 'forum/comment.html'

class Register(CreateView):
    model = User
    form_class = RegisterForm
    template_name = 'forum/register.html'

    def form_valid(self, form):
        
        response = super().form_valid(form)
        user = self.object
        login(self.request, user)
        return response
    
    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.object.pk})

class Post(LoginRequiredMixin, CreateView):
    form_class = PostForm
    template_name = 'forum/post.html'
    success_url = reverse_lazy('comments')

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

URL とクラスベースビューのマッピング

最後に、アプリがリクエストを受け取った際に、ここで定義したクラスベースのビューが実行されるように urls.py の変更を行いたいと思います。変更の仕方は urls.py を作成する で示した通りで、関数ベースビューの場合は urlpatterns の要素に指定する path 関数の第2引数に 関数オブジェクト をそのまま指定する必要がありましたが、クラスベースビューの場合は path 関数の第2引数には クラスオブジェクト.as_view() を指定する必要があります。

要は、変更前の urls.py における 関数オブジェクト の部分を、置き換え後のクラスの クラスオブジェクト.as_view() に書き換えてやれば良いです。

変更前の urls.py は下記のようになっており、各 path 関数の第2引数には views.index_view などの関数オブジェクトを指定しているため、これらを views.Index.as_view() などのクラスオブジェクトの as_view メソッド実行結果に置き換えてやれば良いです。

変更前のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index_view, name='index'),
    path('comments/', views.comments_view, name='comments'),
    path('comment/<int:comment_id>/', views.comment_view, name='comment'),
    path('users/', views.users_view, name='users'),
    path('user/<int:user_id>/', views.user_view, name='user'),
    path('register/', views.register_view, name='register'),
    path('post/', views.post_view, name='post'),
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
]

具体的には下記のように変更してやれば、第1引数で指定した URL (URL パターン) へのリクエストを受け取った際に、第2引数で指定したクラスが動作することになります。

変更後のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.Index.as_view(), name='index'),
    path('comments/', views.CommentList.as_view(), name='comments'),
    path('comment/<int:comment_id>', views.CommentDetail.as_view(), name='comment'),
    path('users/', views.UserList.as_view(), name='users'),
    path('user/<int:user_id>', views.UserDetail.as_view(), name='user'),
    path('register/', views.Register.as_view(), name='register'),
    path('post/', views.Post.as_view(), name='post'),
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'),
]

以上で、関数ベースビューからクラスベースビューへの置き換えは完了です。動作確認の説明は省略しますが、是非、クラスベースに置き換えたビューで、関数ベースビューの時同様の動作が実現できていることを確認してみていただければと思います。

まとめ

このページでは、クラスベースビューについて解説しました!

クラスベースビューとはクラスの定義によって作成されるビューです。それに対し、関数の定義によって作成されるビューは関数ベースビューと呼ばれます。クラスベースビューは品質や保守性の面で関数ベースビューよりも優れています。

また、クラスベースビューは Django フレームワークで定義される View のサブクラス を継承するクラスを定義し、開発するウェブアプリに合わせてカスタマイズしていくことで実装していくことになります。View のサブクラス には典型的なビューの処理の流れが実装されており、この処理の流れをクラス変数の定義やメソッドのオーバーライドによって開発するウェブアプリに応じたものにカスタマイズすることができるようになっています。

ただし、View のサブクラス によって定義されているクラス変数やメソッドが異なるため、View のサブクラス によってカスタマイズの仕方が異なることになります。なので、各 View のサブクラス についても知識があったほうが良いです。この辺りは、View のサブクラス ごとに解説ページを設けて説明していますので、興味があれば下記ページも読んでみていただければと思います!

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現) DjangoのDetailViewの解説ページアイキャッチ 【Django】DetailViewの使い方(クラスベースビューでの詳細ページの実現) CreateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】CreateViewの使い方(クラスベースビューでの新規登録ページの実現) UpdateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】UpdateViewの使い方(クラスベースビューでのレコード更新ページの実現) DeleteViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】DeleteViewの使い方(クラスベースビューでのレコード削除ページの実現) FormViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】FormViewの使い方(クラスベースビューで汎用的なフォームを扱う) LoginViewの使い方の解説ページアイキャッチ 【Django】LoginViewの使い方(クラスベースビューでのログインの実現) LogoutViewの使い方の解説ページアイキャッチ 【Django】LogoutViewの使い方(クラスベースビューでのログアウトの実現)

同じカテゴリのページ一覧を表示