Django のクラスベースビューを完全に理解する(v5.1 対応)
昔書いた記事を最新版対応にして載せ直します。
はじめに
Django での開発時によく使うのが、クラスベースビューです。
クラスベースビューは「クラスベース汎用ビュー」または「汎用ビュー」と呼ばれることもありますが、データモデルの CRUD のような基本的な機能を簡単に実装できるように、ビューを再利用できるクラスとして提供したものです。
しかし現実のアプリケーションはデータモデルの CRUD のような基本的な機能だけではなく、例えば「2 つのデータモデルを同時に編集させたい」「一覧を表示しつつ新規入力もさせたい」のような、単なる CRUD ではない機能が要求されることもあります。そのような要求が来るたびに開発者は「これはどのクラスベースビューを使えばいいか」を考える必要があります。
このときに間違った判断をすると、後から見て何をやっているのか分からないコードになったり、実装はできたものの、コードが冗長になったりします。そのため、どのクラスをベースに作るかを判断するために、クラスベースビューの仕組みを理解しておく必要があります。
そしてクラスベースビューの仕組みを理解するためには、自分はソースコードを読むのが一番だと考えています。しかし、クラスベースビューはクラス構成が複雑なため、前提知識なしにソースコードを読むのは骨が折れます。
そのため、ソースコードを読むための前提知識として、クラスベースビューのクラス構成を解説するのがこの記事の目的です。そのため、各クラスが提供する機能については詳しく解説しません。
Django のクラスベースビューはどれだけあるか
Django のクラスベースビューを調べるときに役立つのが、Django Class-Based-View Inspectorです。このサイトでは、Django で使われているクラスベースビューを用途別にまとめています。また、クラス名をクリックすることで、クラスの詳細、具体的には継承関係、属性、メソッドが表示されます。
このサイトによれば、Django には 24 個のクラスベースビューがあります。しかしよく使うクラスベースビューは限られています。そのためこの記事では、よく使われる次のクラスベースビュー 9 個に限って解説します。
- Generic Base
- モジュール: django.views.generic.base
- クラス: View, TemplateView, RedirectView
- Generic Detail
- モジュール: django.views.generic.detail
- クラス: DetailView
- Generic List
- モジュール: django.views.generic.list
- クラス: ListView
- Generic Edit
- モジュール: django.views.generic.edit
- クラス: FormView, CreateView, UpdateView, DeleteView
Django のクラス構成
まず、9 個のクラスベースビューには、親クラスが 16 個あります。クラスベースビューとその親クラスを合わせて 25 個のクラスの継承関係をクラス図にすると次のようになります。
classDiagram namespace base { class ContextMixin class View class TemplateResponseMixin class TemplateView class RedirectView } namespace detail { class SingleObjectMixin class BaseDetailView class SingleObjectTemplateResponseMixin class DetailView } namespace list { class MultipleObjectMixin class BaseListView class MultipleObjectTemplateResponseMixin class ListView } namespace edit { class FormMixin class ModelFormMixin class ProcessFormView class BaseFormView class FormView class BaseCreateView class CreateView class BaseUpdateView class UpdateView class DeletionMixin class BaseDeleteView class DeleteView } View <|-- TemplateView ContextMixin <|-- TemplateView TemplateResponseMixin <|-- TemplateView SingleObjectTemplateResponseMixin <|-- CreateView BaseCreateView <|-- CreateView ModelFormMixin <|-- BaseCreateView ProcessFormView <|-- BaseCreateView FormMixin <|-- ModelFormMixin SingleObjectMixin <|-- ModelFormMixin ContextMixin <|-- FormMixin TemplateResponseMixin <|-- FormView BaseFormView <|-- FormView SingleObjectTemplateResponseMixin <|-- DetailView BaseDetailView <|-- DetailView SingleObjectMixin <|-- BaseDetailView View <|-- BaseDetailView MultipleObjectTemplateResponseMixin <|-- ListView BaseListView <|-- ListView SingleObjectTemplateResponseMixin <|-- UpdateView BaseUpdateView <|-- UpdateView ModelFormMixin <|-- BaseUpdateView ProcessFormView <|-- BaseUpdateView FormMixin <|-- BaseFormView ProcessFormView <|-- BaseFormView BaseDeleteView <|-- DeleteView SingleObjectTemplateResponseMixin <|-- DeleteView MultipleObjectMixin <|-- BaseListView View <|-- BaseListView View <|-- ProcessFormView DeletionMixin <|-- BaseDeleteView FormMixin <|-- BaseDeleteView BaseDetailView <|-- BaseDeleteView ContextMixin <|-- SingleObjectMixin TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin ContextMixin <|-- MultipleObjectMixin TemplateResponseMixin <|-- MultipleObjectTemplateResponseMixin View <|-- RedirectView
見るのも嫌になりますよね。このように複雑な継承関係になる理由は、Python が多重継承を採用しているからです。
多重継承は名前こそシンプルですが、菱形継承問題などの問題を引き起こしやすい機能です。そのため、最近作られた言語では多重継承は採用されません(Python は 1991 年に誕生した、歴史ある言語です)。
Django では多重継承に関する問題を避けるために、クラス構成を工夫することで、実質的に単一継承にしています。具体的には、Django のクラスベースビューで使われるクラスを「View クラス」と「Mixin クラス」の大きく 2 つに分けています。そして、木の幹となる「View クラス」は単一継承とし、木の枝となる「Mixin クラス」を複数付けられるようにしています。
View クラスは木の幹に相当するもので、次の特徴があります。
- django.views.generic.base.View クラスを頂点とした単一継承
- View クラスのみでビューが作れる
- クラス名の最後に「View」が付いている
- 頂点となる django.views.generic.base.View クラスを除いて、get(), post()など、HTTP メソッドに対応したメソッドのみオーバーライドされている
View クラスのみをクラス図にすると次のようになります。
classDiagram View <|-- TemplateView BaseCreateView <|-- CreateView ProcessFormView <|-- BaseCreateView BaseFormView <|-- FormView BaseDetailView <|-- DetailView View <|-- BaseDetailView BaseListView <|-- ListView BaseUpdateView <|-- UpdateView ProcessFormView <|-- BaseUpdateView ProcessFormView <|-- BaseFormView BaseDeleteView <|-- DeleteView View <|-- BaseListView View <|-- ProcessFormView BaseDetailView <|-- BaseDeleteView View <|-- RedirectView
一方で Mixin クラスは木の枝に相当するもので、次の特徴があります。Java の Interface、Ruby の Module と似たようなものです。
- 特定の機能だけを実装した「部品」を提供する
- Mixin クラスだけでビューは作れない
- クラス名の最後に「Mixin」が付いている
Mixin クラスだけをクラス図にすると次のようになります。
classDiagram class ContextMixin class TemplateResponseMixin class SingleObjectMixin class SingleObjectTemplateResponseMixin class MultipleObjectMixin class MultipleObjectTemplateResponseMixin class FormMixin class ModelFormMixin class DeletionMixin FormMixin <|-- ModelFormMixin SingleObjectMixin <|-- ModelFormMixin ContextMixin <|-- FormMixin ContextMixin <|-- SingleObjectMixin TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin ContextMixin <|-- MultipleObjectMixin TemplateResponseMixin <|-- MultipleObjectTemplateResponseMixin
再度まとめると、全ての View クラスは頂点となる django.views.generic.base.View クラスを除いて、ただ 1 個の View クラスと、0 個以上の Mixin クラスを継承しています。複数の View クラスを継承することはありません。
この仕組みが理解できると、Django のクラス構成が理解しやすくなります。
メソッド解決順序
また、クラスを多重継承する場合、同じメソッドが継承元の複数のクラスで定義されることがあります。そのため、どのクラスのメソッドが優先的に呼ばれるのか、その順番を決める必要があります。
そのメソッドが優先的に呼ばれる順番が「メソッド解決順序」です。英語では Method Resolution Order で、MRO と略します。
そして、Python のメソッド解決順序は次の 2 つの条件を満たしています。
- 派生クラスが基底クラスより優先される
- 継承の定義で左側に書いた基底クラスが右側に書いた基底クラスより優先される
例えば TemplateView は次のように定義されています。
class TemplateView(TemplateResponseMixin, ContextMixin, View):
このときに、メソッド解決順序は次のようになります。
Generic Base
まずは基本となるクラスを集めた、Generic Base について解説します。 Generic Base で解説するクラスはモジュール django.views.generic.base で定義されています。
View
まずは全ての View クラスの頂点となる、django.views.generic.base.View クラスについて解説します。
これ以降のクラス図ではインスタンス変数、クラスメソッド、インスタンスメソッドについて記載しますが、メソッドの引数および戻り値は省略します。
classDiagram class View { http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] request args kwargs view_is_async$ as_view()$ dispatch() setup() http_method_not_allowed() options() }
大まかな処理の流れは次のようになっています。
- [as_view()]
- [setup()][View.setup()] で request, args, kwargs をインスタンス変数として登録
- [dispatch()][View.dispatch()] を呼び出す
- [dispatch()] メソッドに対応するメソッドを探して呼び出す(例: GET→get()、POST→post())
- ただし次の場合は [http_method_not_allowed()][View.http_method_not_allowed()] を呼び出す
- HTTP メソッドが [http_method_names][View.http_method_names] に未定義
- HTTP メソッドに対応するメソッドが見つからない
- OPTIONS については定義ずみ([options()][View.options()])
- ただし次の場合は [http_method_not_allowed()][View.http_method_not_allowed()] を呼び出す
RedirectView
次に、リダイレクトを行うためのクラスベースビューである、RedirectView を解説します。クラス図は次のようになります。
classDiagram class RedirectView { permanent url pattern_name query_string get_redirect_url() get() head() post() options() delete() put() patch() } View <|-- RedirectView
いろいろメソッドが定義されていますが、 [head()][View.head()] 以降は全て [get()][View.get()] を呼び出しており、[get()][View.get()] メソッドではリダイレクトしています。すなわち、どの HTTP メソッドを使ってアクセスしてもリダイレクトされます。
TemplateView
次に、シンプルなクラスベースビューの 1 つである、 TemplateView を解説します。クラス図は次のようになります。
classDiagram class TemplateResponseMixin { template_name template_engine response_class content_type render_to_response() get_template_names() } class ContextMixin { extra_context get_context_data() } class TemplateView { get() } TemplateResponseMixin <|-- TemplateView ContextMixin <|-- TemplateView View <|-- TemplateView
TemplateView はテンプレート機能に対応したクラスベースビューです。TemplateView は 2 つの機能からなっており、それぞれ継承している 2 つの Mixin クラスで実装されています。
- TemplateResponseMixin: テンプレートファイルを指定する機能
- ContextMixin: テンプレートファイルに渡すコンテキストを指定する機能
そして、 TemplateView の [get()][TemplateView.get()] メソッドは次のように定義されています。コンテキストを取得して、テンプレートに渡すだけのシンプルな処理です。
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
Generic Detail
次に、詳細を表示するクラスを集めた、Generic Detail について解説します。Generic Detail で解説するクラスはモジュール django.views.generic.detail で定義されています。
DetailView
データモデルを表示するためのクラスベースビューである、DetailView を解説します。クラス図は次のようになります。
classDiagram class TemplateResponseMixin { template_name template_engine response_class content_type render_to_response() get_template_names() } class BaseDetailView { object get() } class ContextMixin { extra_context get_context_data() } class SingleObjectMixin { model queryset slug_field = 'slug' context_object_name slug_url_kwarg = 'slug' pk_url_kwarg = 'pk' query_pk_and_slug get_object() get_queryset() get_slug_field() get_context_object_name() get_context_data() } class SingleObjectTemplateResponseMixin { template_name_field template_name_suffix = '_detail' get_template_names() } class DetailView SingleObjectTemplateResponseMixin <|-- DetailView BaseDetailView <|-- DetailView SingleObjectMixin <|-- BaseDetailView View <|-- BaseDetailView ContextMixin <|-- SingleObjectMixin TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
ContextMixin, TemplateResponseMixin は TemplateView で出てきました。その他の Mixin クラスについては次のような役割です。
- SingleObjectMixin: 単一のデータモデルを取得する
- SingleObjectTemplateResponseMixin: 単一のデータモデルに対応したテンプレート名を取得する
そしてこれがややこしいのですが、 SingleObjectTemplateResponseMixin を使うには次のいずれかが必須です。
- self.template_name が定義されていること
- メソッド get_template_names() が値を返すこと
- self.object が定義されていること
しかし、 SingleObjectTemplateResponseMixin では self.object は定義されていません。この self.object はどこから来るのでしょうか。
それは BaseDetailView です。 [get()][BaseDetailView.get()] メソッドは次のように定義されており、 self.object がここで設定されていること、コンテキスト取得時にそのオブジェクトを渡していることが分かります。
なお、 DetailView ではメソッドはオーバーライドされていません。
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
Generic List
次に、一覧表示を行うクラスを集めた、Generic List について解説します。Generic List で解説するクラスはモジュール django.views.generic.list で定義されています。
ListView
データモデルを一覧表示するためのクラスベースビューである、 ListView を解説します。クラス図は次のようになります。
classDiagram class TemplateResponseMixin { template_name template_engine response_class content_type render_to_response() get_template_names() } class BaseListView { object_list get() } class ContextMixin { extra_context get_context_data() } class MultipleObjectMixin { allow_empty queryset model paginate_by paginate_orphans context_object_name paginator_class page_kwarg = 'page' ordering get_queryset() get_ordering() paginate_queryset() get_paginate_by() get_paginator() get_paginate_orphans() get_allow_empty() get_context_object_name() get_context_data() } class MultipleObjectTemplateResponseMixin { template_name_suffix = '_list' get_template_names() } class ListView MultipleObjectTemplateResponseMixin <|-- ListView BaseListView <|-- ListView MultipleObjectMixin <|-- BaseListView View <|-- BaseListView ContextMixin <|-- MultipleObjectMixin TemplateResponseMixin <|-- MultipleObjectTemplateResponseMixin
勘のいい人なら気づくかもしれませんが、 ListView は DetailView と構成がほぼ同じで、クラスが以下のように変わっただけです。
- SingleObjectMixin → MultipleObjectMixin
- SingleObjectTemplateResponseMixin → MultipleObjectTemplateResponseMixin
- BaseDetailView → BaseListView
- DetailView → ListView
主な違いは次の通りです。
- MultipleObjectTemplateResponseMixin は self.template_name, get_template_names() あるいは self.object_list の存在を仮定
- ページネーション機能がある
なお、 ListView ではメソッドはオーバーライドされていません。
Generic Edit
最後に、編集を行うクラスを集めた、Generic Edit について解説します。Generic Edit で解説するクラスはモジュール django.views.generic.edit で定義されています。
FormView
フォームを扱うためのクラスベースビューである、 FormView を解説します。最も複雑で、理解が難しいのがこの FormView です。クラス図は次のようになります。
classDiagram class TemplateResponseMixin { template_name template_engine response_class content_type render_to_response() get_template_names() } class ContextMixin { extra_context get_context_data() } class FormMixin { initial form_class success_url prefix get_initial() get_prefix() get_form_class() get_form() get_form_kwargs() get_success_url() form_valid() form_invalid() get_context_data() } class BaseFormView class View class ProcessFormView { get() post() put() } class FormView TemplateResponseMixin <|-- FormView ContextMixin <|-- FormMixin FormMixin <|-- BaseFormView View <|-- ProcessFormView ProcessFormView <|-- BaseFormView BaseFormView <|-- FormView
FormMixin はフォーム作成に必要な情報を作成しています。フォームをカスタマイズするときは、この FormMixin で定義されているメソッドをオーバーライドすることが多いでしょう。そして処理の流れは ProcessFormView で定義されています。 BaseFormView と、 FormView ではメソッドはオーバーライドされていません。
CreateView
次に、データモデルを作成するためのクラスベースビューである、 CreateView を解説します。クラス図は次のようになります。
classDiagram class TemplateResponseMixin { template_name template_engine response_class content_type render_to_response() get_template_names() } class SingleObjectTemplateResponseMixin { template_name_field template_name_suffix = '_detail' get_template_names() } class ContextMixin { extra_context get_context_data() } class FormMixin { initial form_class success_url prefix get_initial() get_prefix() get_form_class() get_form() get_form_kwargs() get_success_url() form_valid() form_invalid() get_context_data() } class SingleObjectMixin { model queryset slug_field context_object_name slug_url_kwarg pk_url_kwarg = 'pk' query_pk_and_slug get_object() get_queryset() get_slug_field() get_context_object_name() get_context_data() } class ModelFormMixin { object form_class fields get_form_class() get_form_kwargs() get_success_url() form_valid() } class View class ProcessFormView { get() post() put() } class BaseCreateView { object = None get() post() } class CreateView { template_name_suffix = '_form' } TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin SingleObjectTemplateResponseMixin <|-- CreateView ContextMixin <|-- FormMixin BaseCreateView <|-- CreateView ModelFormMixin <|-- BaseCreateView ProcessFormView <|-- BaseCreateView FormMixin <|-- ModelFormMixin SingleObjectMixin <|-- ModelFormMixin View <|-- ProcessFormView ContextMixin <|-- SingleObjectMixin
継承関係は複雑ですが、新規に出てくる Mixin クラスは ModelFormMixin ただ 1 つです。この Mixin クラスでは、データモデル固有の実装になるように、メソッドをオーバーライドしています。
ややこしいのは、 DetailView では self.object の設定は BaseDetailView が行っていましたが、 CreateView では self.object の設定は ModelFormMixin が行っていることです。
UpdateView
次に、データモデルを更新するためのクラスベースビューである、 UpdateView を解説します。クラス図は次のようになります。
classDiagram class TemplateResponseMixin { template_name template_engine response_class content_type render_to_response() get_template_names() } class SingleObjectTemplateResponseMixin { template_name_field template_name_suffix = '_detail' get_template_names() } class ContextMixin { extra_context get_context_data() } class FormMixin { initial form_class success_url prefix get_initial() get_prefix() get_form_class() get_form() get_form_kwargs() get_success_url() form_valid() form_invalid() get_context_data() } class ModelFormMixin { object form_class fields get_form_class() get_form_kwargs() get_success_url() form_valid() } class SingleObjectMixin { model queryset slug_field context_object_name slug_url_kwarg pk_url_kwarg = 'pk' query_pk_and_slug get_object() get_queryset() get_slug_field() get_context_object_name() get_context_data() } class BaseUpdateView { object get() post() } class UpdateView { template_name_suffix = '_form' } class ProcessFormView { get() post() put() } FormMixin <|-- ModelFormMixin SingleObjectMixin <|-- ModelFormMixin ContextMixin <|-- FormMixin SingleObjectTemplateResponseMixin <|-- UpdateView BaseUpdateView <|-- UpdateView ModelFormMixin <|-- BaseUpdateView ProcessFormView <|-- BaseUpdateView View <|-- ProcessFormView ContextMixin <|-- SingleObjectMixin TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
これは CreateView が理解できれば簡単です。なぜなら、継承関係がほぼ同じだからです。実装もほぼ同じで、 BaseCreateView が次のような実装なのに対し、
def get(self, request, *args, **kwargs):
self.object = None
return super().get(request, *args, **kwargs)
BaseUpdateView が次のような実装になっています。
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
この実装の違いは、 CreateView が初期状態を持たないのに対して、 UpdateView が初期状態を持つ、それだけです。詳しくは ModelFormMixin の実装を確認してください。
DeleteView
最後に、データモデルの削除を行うためのクラスベースビューである、 DeleteView を解説します。クラス図は次のようになります。
classDiagram class DeleteView { template_name_suffix = '_confirm_delete' } class ContextMixin { extra_context get_context_data() } class DeletionMixin { delete() post() get_success_url() } class FormMixin { initial form_class success_url prefix get_initial() get_prefix() get_form_class() get_form() get_form_kwargs() get_success_url() form_valid() form_invalid() get_context_data() } class SingleObjectMixin { model queryset slug_field context_object_name slug_url_kwarg pk_url_kwarg = 'pk' query_pk_and_slug get_object() get_queryset() get_slug_field() get_context_object_name() get_context_data() } class SingleObjectTemplateResponseMixin { template_name_field template_name_suffix = '_detail' get_template_names() } class TemplateResponseMixin { template_name template_engine response_class content_type render_to_response() get_template_names() } class BaseDetailView { object get() } class BaseDeleteView View <|-- BaseDetailView SingleObjectMixin <|-- BaseDetailView BaseDeleteView <|-- DeleteView SingleObjectTemplateResponseMixin <|-- DeleteView DeletionMixin <|-- BaseDeleteView FormMixin <|-- BaseDeleteView BaseDetailView <|-- BaseDeleteView ContextMixin <|-- FormMixin ContextMixin <|-- SingleObjectMixin TemplateResponseMixin <|-- SingleObjectTemplateResponseMixin
DeleteView の特徴は、 BaseDetailView を継承していることです。そして、 BaseDetailView の実装は3.2までと、4.0以降では大きく変わっています。
3.2のときには BaseDeleteView では何もオーバーライドしておらず、 DetailView に DeletionMixin が追加されているだけでした。
4.0以降では BaseDeleteView は FormMixin も継承しています。4.0のリリースノートのGeneric Viewsの記述 では、削除を確認するためのチェックボックスや、成功時のメッセージ提供に使うことを意図しているようです。
DeletionMixin はその名のとおり、削除機能を実装していますが、 BaseDetailView を継承する理由は何でしょうか。それは、GET メソッドでアクセスすると、データモデルの内容が入った確認画面を表示し、削除ボタンを押して POST メソッドでアクセスすると削除される、それが DeleteView の標準の仕様だからです。
おわりに
Django のクラスベースビューについて一通り解説しました。
この記事を読んだあとは、Django 公式サイトにあるドキュメント、Using mixins with class-based viewsを読んでみてください。この公式ドキュメントでは、クラスベースビューや Mixin を使ってどのように必要な機能を実装していくかの指針が書かれています。
逆に言えば、この指針が理解できれば「これはどのクラスベースビューを使えばいいか」を判断できるようになります。そしてこの記事が、この指針の理解の助けになれば幸いです。