採用はこちら!

Shinonome Tech Blog

株式会社Shinonomeの技術ブログ
7 min read

クラスベース汎用ビューのオーバーライドの方法

Djangoにおけるクラスベース汎用ビューのオーバーライドの順序を紹介します。 実現したい挙動をコードに起こすための手順を整理し、同時にDjangoへの理解を深めることを目的とします。

 アドベントカレンダー10日目🌟
 バックエンドコースの koichi と hanaka です。現在、Djangoを用いてTwitterのクローンアプリを作成するという最終課題に取り組んでいます。コース課題進行中に先輩から教わってフレームワークの凄さを実感した、クラスベースビューの裏側について取り上げて書きました。

内容

クラスベース汎用ビューのオーバーライドの順序を、1つの例とともに紹介します。
 実現したい挙動をコードに起こすためにはどのような手順を踏めばいいのかを整理し、Djangoにおけるクラスベースビューの読み方と書き方の理解を深めることを目的とします。

用いる例

 Twitterのようなアプリのプロフィールページの実装を例に考えます。
 “ホーム”ページにて投稿上部に表示されるユーザーネームをクリックすると、投稿したユーザーの”プロフィール”ページに遷移する動作を実装していきます。
 遷移したページには投稿をしたユーザー "sample1" の投稿リストを表示します。

完成イメージ(”ホーム”ページ)
完成イメージ(”プロフィール”ページ)

Viewで実現したい挙動

 実現したい挙動をもとに上書きが必要な部分を探っていくので、まずは今回上書きしていく View で実現したい挙動を大きく2つの項目に分けて以下に整理します。
・ テンプレートを表示する
・ 特定のユーザーのすべてのツイートを取得する
  1. ユーザーネームをURLの末尾から取得する
  2. ユーザーネームからユーザーの情報を取って来る
  3. 取って来たユーザーの情報から、そのユーザーのツイートを取得する
 最終的には accounts / views.py に以下のような記述をします。

class UserProfileView(LoginRequiredMixin, DetailView):
    model = User
    template_name = "profile.html"
    slug_field = "username"
    slug_url_kwarg = "username"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        user = self.object
        context["tweet_list"] = (
            Tweet.objects.select_related("user")
            .filter(user=user)  
        )
        return context

オーバーライドの手順

オーバーライドとは?

 親クラスを継承したクラスは、独自の属性とメソッドも定義して使用できます。親クラスで定義されているメソッドをそのまま使用することもできますが、その親クラスのメソッドを継承するクラス内で同名のメソッドを定義することで上書きすることが可能です。これを「オーバーライド」といいます。

 今回の例では、ユーザー情報の詳細を見ることを目的としているので DetailView を継承して、必要に応じてオーバーライドしていきます。
 説明の中で使用するリポジトリの構成は下図の通りです。

twitter-like-app
  -accounts
    -urls.py
    -views.py
  -templates
    -accounts
      -profile.html
DetailView -- Classy CBV
DetailView in Django 4.1. Render a “detail” view of an object. By default this is a model instance looked up from `self.queryset`, but theview will support display of *any* object by overriding `self.get_object()`.
DetailViewのソースコード

テンプレートとモデルの設定

 始めに、汎用ビューにはあらかじめ設定することのできる変数があります。以下の2つの変数はデフォルトではNone、つまり何も指定されていないので、取り出してきて上書きをします。

template_name = "accounts/profile.html" # 1
model = User # 2

 まず、使用したいテンプレートのファイルを指定します( # 1 )。これによって実現したい挙動の1つ目、テンプレートの表示は達成されます。これ以降は実現したい挙動の2つ目を作成していきます。
 また、今回はユーザーの情報が格納された、Userというモデルを用います( # 2 )。

 次に、関数を順に追っていきます。
 まずリクエスト・レスポンス処理の入口であるas_viewメソッドが走り、setupメソッド、dispachメソッドと続き、その次に DetailView の getメソッドが呼ばれます。as_viewメソッド、setupメソッド、dispatchメソッドは上書きしないので今回は省略します。
 getメソッドは get の処理の内容が書かれた核となっています。上書きはしませんが、このメソッドのコードを追うことで、上書きが必要な部分を探っていきます。

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)

get_object

 DetailView の get_objectメソッドでは、リクエストURLに基づいてオブジェクトを作成します。今回はURLのエンドポイントに設定したユーザーネームをキーワードとして、ユーザーの情報を返します。
 このURLの末尾の部分を任意の文字列に指定できる機能を slug といいます。

http://127.0.0.1:8000/accounts/sample1/
                                ↑これ 
# URLの末尾( = slug )に任意の文字列である username が入るように設定している。
urlpatterns = [
    path("<str:username>/", views.UserProfileView.as_view(), name="user_profile"),
]

[参考] accounts/urls.py

 一方 slug と同時に登場することが多々ある pk ( = primary key ) とは、各データのユニークを表す、DB内で pk = TRUE となっている要素のことを指します。
 デフォルトの get_objectメソッドでは pk を参照する仕様になっているため、slug を使えるように上書きしていきます。

def get_object(self, queryset=None):
    # queryset = None なのでこのif文に入る。    # 1
    if queryset is None:
        queryset = self.get_queryset()
				
    # ↓ urlからpkが取得できないため、pk = None
    pk = self.kwargs.get(self.pk_url_kwarg)

    slug = self.kwargs.get(self.slug_url_kwarg)    # 2
				
    # ↓ pk = Noneであるので、このif文には入らない。
    if pk is not None:
        queryset = queryset.filter(pk=pk)

    # ↓ slug = userame and pk = None なのでこのif文に入る。    # 3
    if slug is not None and (pk is None or self.query_pk_and_slug): 
        slug_field = self.get_slug_field() 
        queryset = queryset.filter(**{slug_field: slug}) 

    """ ここまでが、slugに入っているユーザーネームを DB内のユーザーネームに紐づけて
        クエリを取り出してくる処理 """


    """ 以下は、万が一 slug に存在しないユーザーネームが入りプロフィールページが
        表示できない場合の処理 """

    if pk is None and slug is None:
        raise AttributeError(
            "Generic detail view %s must be called with either an object "
            "pk or a slug in the URLconf." % self.__class__.__name__
        )

    try:
        obj = queryset.get()
    except queryset.model.DoesNotExist:
        raise Http404(
            _("No %(verbose_name)s found matching the query")
            % {"verbose_name": queryset.model._meta.verbose_name}
        )
    return obj

    """ 本来なら queryset.model.DoesNotExist という500番のサーバーエラーが出てしまうが、
        デフォルトで404を出す処理に変更してくれているので上書きは不要 """
# 1 get_queryset

 get_object が呼ばれた段階ではクエリセット内にデータが入っていません。実現したい挙動に基づいて投稿したユーザーのデータだけを取得していきたいのですが、その前提として、先ほど model変数に設定した Userモデル内にある全てのデータを呼ぶ処理がこのメソッドで行われます。

def get_queryset(self):
    if self.queryset is None:
        if self.model:
            return self.model._default_manager.all()
        else:
            raise ImproperlyConfigured(
                "%(cls)s is missing a QuerySet. Define "
                "%(cls)s.model, %(cls)s.queryset, or override "
                "%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
            )
    return self.queryset.all()
# 2 slug = self.kwargs.get(self.slug_url_kwarg)

 get_slug_url_kwargメソッドでは slug_url_kwarg変数を呼んでいるので、slug_url_kwarg変数の上書きをします。
 URLの slug には Userモデルの usernameキーに対応する値を利用しているため、slug_url_kwarg には任意の文字列 "username" が入ります。

    slug_url_kwarg = "slug"
    #  ↓↓↓
    slug_url_kwarg = "username"
# 3 slug_field、queryset

 ここでは slug_field を定義した上で、slug_field から取って来た "username" と先ほどslug変数に上書きした "username" が一致したユーザーのデータ、つまり投稿をしたユーザーの情報のみを queryset としています。

def get_slug_field(self):
    return self.slug_field

 get_slug_fieldメソッドでは slug_field変数を呼んでいるため、slug_field変数の上書きをします。
 URLの slug は Tweetモデルの usernameフィールドにあるデータを利用しているため、slug_field に "username" が入るように上書きをします。

    slug_field = "slug"
    #  ↓↓↓  
    slug_field = "username"

 ここまで、get_objectメソッドを1つずつ追った上で、変数単位で必要な上書きをしました。

get_context_data

 次に get_context_dataメソッドが呼ばれます。

def get_context_data(self, **kwargs):

    context = {}
    
    if self.object:
        context["object"] = self.object
	    context_object_name = self.get_context_object_name(self.object)
        
    if context_object_name:
        context[context_object_name] = self.object
    context.update(kwargs)

    return super().get_context_data(**context)

 このメソッドでは get_object で取得したオブジェクトからコンテキストを作成します。
汎用ビューに書かれている処理のみだと、
  context[”object”] = self.object
self.objectの実体は get_objectメソッドで取得した、”sample1”(特定のユーザー)のユーザー情報であり、
  context["object"] = User.filter(usename="sample1")
というようにすることで、”object” という名前の ”sample1” のユーザー情報のコンテキストをcontextに追加しています。ただ、これでは、はじめに model= に設定した Userオブジェクトに関するコンテキストしか作成してくれません。

 Tweetオブジェクトから ”sample1”(特定のユーザー)の全投稿を取得して表示したいので、既存の get_context_dataメソッドの処理にさらにそのコンテキストを追加する処理を書きたいのです。そのため、UserProfileView ではこの get_context_data をメソッドごと書き換えます。

class UserProfileView(LoginrequiredMixin, DetailView)
    """
    省略
    """
    def get_context_data(self, **kwargs):                     
    context = super().get_context_data(self.object)		                   # 1
        user = self.object									               # 2
    context["tweet_list"] = Tweet.filter(user=user).select_related("user")
        return context										               # 3
  1. 既存の context は DetailView の get_context_data() に作ってもらう。
    まずは既存の( SingleObjectMixin という親クラスに記述された)get_context_data() を super() を使うことで呼び、context に context["object"] = User.filter( usename="sample1" ) などが追加されたものを context として用意します。( # 1 )

  2. 用意した context に対し、Tweetオブジェクトのコンテキストを追加。
    context[”tweet_list”] には Tweetオブジェクトからそのユーザーのツイートだけを取得し、ユーザーの情報と紐付けたものをコンテキストとして追加します。( # 2 ) のように記述することでで、”tweet_list” という名前の Tweetオブジェクトのうちユーザー ”sample1” (特定のユーザー)のものだけを取り出し、ユーザー情報と紐付けたものをコンテキストとして追加します。

  3. context を返り値として返す。( # 3 )

このように上書きし、すでにある汎用ビューのメソッドがデフォルトで作成してくれるコンテキストを用意した上で、そこに追加したいコンテキストを追加することができました。


最後に

  Djangoは広く用いられているフレームワークなので、典型的なカスタマイズの方法については様々なブログなどでコードの例が紹介されています。ですが、自分が作成したい機能にぴったり合ったコードを簡潔に書くため、そしてDjangoへの理解を深めるためには、実際に自分でソースコードを追って上書きする部分を見つけ出すことが必要だと感じました。
 まだ自分たちも独力ではきれいに書けないからこそ、今回このようにまとめてみるに至りました。色々と内容を追加していたら長くなりましたが、この記事が少しでも手助けになれば嬉しいです。
 この記事の作成に際し、ご助力くださいましたバックエンドコース講師の masayaさん、akinoriさんに感謝申し上げます。


参考文献

【Python連載】メソッドのオーバーライド | TECH PROjin
ITを学ぶなら「テックプロジン」
スラッグとは|グラフィックデザイン・グラフィックデザイナー専攻|デジタルハリウッドの専門スクール(学校)
【django】モデルのフィールドについて:フィールドの型・オプション一覧 | OFFICE54
本記事ではdjangoのモデルにおける、フィールド(フィールドの型・フィールドオプション)について詳しく解説していきます。djangoではmodels.py内でモデル定義を行い、このモデルを通してデータベース操作を行います。
500 Internal Server Error - HTTP | MDN
HyperText Transfer Protocol (HTTP) の 500 Internal Server Error サーバーエラーレスポンスコードは、サーバーがリクエストを実行を妨げる予期しない条件に遭遇したことを示します。