LoginSignup
1
2

DjangoのListViewクラスについて理解する

Last updated at Posted at 2023-10-31

はじめに

ListViewはとても簡単にリスト表示やページネーション機能を実装することができます。
基本的に属性を指定すれば動くのですが、コードを理解しないと柔軟に対応できない恐れがあります。
本記事で理解いただければ幸いです。

ListViewの全体コードはこちら

ListViewの処理の流れ

ListViewはMultipleObjectTemplateResponseMixinBaseListViewを継承しています。
継承順により、MultipleObjectTemplateResponseMixinが先に実行され、その後にBaseListViewの処理が行われます。

class ListView(MultipleObjectTemplateResponseMixin, BaseListView):

継承① MultipleObjectTemplateResponseMixin

このMixinはTemplateResponseMixinを継承し、テンプレートの指定やHTTPレスポンスの生成を行います。

継承② BaseListView

MultipleObjectMixinViewを継承しており、ここでリストの取得やページネーション、コンテキストデータの生成が行われます。
Viewクラスのas_view()メソッドにより、適切なHTTPメソッドへのマッピングが行われます。

MultipleObjectMixinクラス

このクラスでは、リストの取得やページネーション、コンテキストデータの生成などが行われます。

class MultipleObjectMixin(ContextMixin):
   allow_empty = True
   queryset = None
   model = None
   paginate_by = None
   paginate_orphans = 0
   context_object_name = None
   paginator_class = Paginator
   page_kwarg = 'page'
   ordering = None

   def get_queryset(self):
       if self.queryset is not None:
           queryset = self.queryset
           if isinstance(queryset, QuerySet):
               queryset = queryset.all()
       elif self.model is not None:
           queryset = 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__
               }
           )
       ordering = self.get_ordering()
       if ordering:
           if isinstance(ordering, str):
               ordering = (ordering,)
           queryset = queryset.order_by(*ordering)

       return queryset

   def get_ordering(self):
       return self.ordering

   def paginate_queryset(self, queryset, page_size):
       paginator = self.get_paginator(
           queryset, page_size, orphans=self.get_paginate_orphans(),
           allow_empty_first_page=self.get_allow_empty())
       page_kwarg = self.page_kwarg
       page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
       try:
           page_number = int(page)
       except ValueError:
           if page == 'last':
               page_number = paginator.num_pages
           else:
               raise Http404(_('Page is not “last”, nor can it be converted to an int.'))
       try:
           page = paginator.page(page_number)
           return (paginator, page, page.object_list, page.has_other_pages())
       except InvalidPage as e:
           raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
               'page_number': page_number,
               'message': str(e)
           })

   def get_paginate_by(self, queryset):
       return self.paginate_by

   def get_paginator(self, queryset, per_page, orphans=0,
                     allow_empty_first_page=True, **kwargs):

       return self.paginator_class(
           queryset, per_page, orphans=orphans,
           allow_empty_first_page=allow_empty_first_page, **kwargs)

   def get_paginate_orphans(self):
       return self.paginate_orphans

   def get_allow_empty(self):
       return self.allow_empty

   def get_context_object_name(self, object_list):
       if self.context_object_name:
           return self.context_object_name
       elif hasattr(object_list, 'model'):
           return '%s_list' % object_list.model._meta.model_name
       else:
           return None

   def get_context_data(self, *, object_list=None, **kwargs):
       queryset = object_list if object_list is not None else self.object_list
       page_size = self.get_paginate_by(queryset)
       context_object_name = self.get_context_object_name(queryset)
       if page_size:
           paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
           context = {
               'paginator': paginator,
               'page_obj': page,
               'is_paginated': is_paginated,
               'object_list': queryset
           }
       else:
           context = {
               'paginator': None,
               'page_obj': None,
               'is_paginated': False,
               'object_list': queryset
           }
       if context_object_name is not None:
           context[context_object_name] = queryset
       context.update(kwargs)
       return super().get_context_data(**context)

クラス変数

変数名 役割
allow_empty クエリセットが空の場合にエラーを出すかどうかを決める変数。デフォルトのTrueならエラーを発生させません
queryset ビューが操作するクエリセットを定義します。設定されていない場合はmodel属性が使われます。
model querysetが設定されていない場合、このモデルからクエリセットが自動的に作成されます。
paginate_by 最後のページに表示するオブジェクトの最小数を指定します。この数未満の場合、それらは前のページに結合されます。
context_object_name テンプレートでクエリセットにアクセスするために使用されるコンテキスト変数の名前を指定します。設定されていない場合、object_listが使用されます。
paginator_class デフォルトはPaginatorクラスで、ページネーションに使用されるクラスを指定します。カスタムページネーターを使用する場合は、この属性をオーバーライドします。
page_kwarg URLからページ番号を取得するために使用されるキーワード引数の名前を指定します。
ordering クエリセットの並び順を指定します。この属性を設定することで、オブジェクトのデフォルトの並び順をオーバーライドできます。

get_querysetメソッド

クエリセットを取得するためのメソッドで、データベースからのデータ取得、フィルタリング、並べ替えを行います。
クエリセットとはデータベースからのデータの取得やフィルタリング、並べ替えなどの操作を抽象的に行うためのものであり、SQLクエリに変換されるので、Pythonのコードとして直感的に操作できるのがクエリセットの大きなメリットです。

クエリセットの例
all_objects = MyModel.objects.all()
filtered_objects = MyModel.objects.filter(name="John")
first_five = MyModel.objects.all()[:5]

コードでは初めにquerysetの存在を確認し、それがQuerySetインスタンスである場合、queryset.all()を使って新しいクエリセットを複製します。
all()メソッドは新しいクエリセットを返し、元のクエリセットに影響を与えないため、ビュー外でqueryset属性が変更されることを防ぎ、ビュー内でのみクエリセットを操作するために行われます。

querysetQuerySetのメソッド(.filter()、.all()など)を持っており、データベースへのクエリが「遅延評価」されることを意味します。
反対に、querysetがリストや他のイテラブルである場合はデータベースの遅延評価のメリットを享受できません。使い道としては特定のリスト操作やクエリセットからデータを即座に取得する必要がある場合に使用する必要があります。
ただし、一般的な使用ではQuerySetインスタンスを使用することを推奨しており。これによりデータベースの効率的な操作や、クエリの遅延評価、およびDjangoの多くの組み込み機能を利用することができます。

クエリセットとそうでない場合の例
# クエリセット
queryset = Article.objects.filter(published=True)
# リスト
queryset = list(Article.objects.filter(published=True))

querysetが存在しない場合はmodelが存在しているかを確認し、存在していればself.model._default_manager.all()でモデルからすべてのオブジェクトを取得します。
いずれも存在しない場合はエラーを出力します。

リストをソートしたい場合は、orderingに値をセットすることで容易に実現することができます。

ソート例
#昇順
class BookListView(ListView):
    model = Book
    ordering = 'title'

#降順
class BookListView(ListView):
    model = Book
    ordering = '-publish_date'

#動的にソートする
class BookListView(ListView):
   model = Book
    def get_ordering(self):
        user_ordering = self.request.GET.get('order')
        if user_ordering == 'desc':
            return '-title'
        return 'title'  

paginate_querysetメソッド

クエリセットをページネーションするためのメソッドで、与えられたクエリセットをページネーションすることができます。

まずはget_paginatorメソッドを使用して新しいPaginatorクラスのインスタンスを作成します。このインスタンスは、クエリセットのページネーションに使用されます。(Paginatorクラスについてはこちら)

取得したいページ番号はpage = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1で取得しており、3つの異なるソースからページ番号を取得することができます。
self.kwargsはビュー関数に渡されるキーワード引数を含む辞書で、URLconfの<int:page>のようなURLのパターンマッチングから得られるキーワード引数を含んでいます。一つ目はそこからpage_kwarg(page)に一致するkeyの値(ページ番号を取得しています。)
self.request.GET.get(page_kwarg)/items/?page=2のようなクエリパラメータからページ番号を取得します。
そして上記二つに当てはまらない場合は1を取得します。
デフォルトのpage_kwargはpageという名前ですが、変更したい場合はビュー内で属性をオーバーライドすることができます。

page_kwargを変更する例
class MyListView(ListView):
    model = MyModel
    paginate_by = 10
    page_kwarg = 'mypage'
path('items/page/<int:mypage>/', MyListView.as_view(), name='item-list-page'),

次に取得したpage(ページ番号)を元に実際にデータを取得していきます。
int型にキャストして、もしエラーになればlastという文字列か確認します。lastの場合、ページ番号はページネータの最後のページとして(paginator.num_pages)として設定されます。num_pagesは、Paginatorクラスのメソッドで、オブジェクトの総数をページあたりのオブジェクト数で割り、その結果を切り上げることで必要な総ページ数を求めることができます。なので、pageがlastの場合、page_number = paginator.num_pagesによってpage_numberに必要なページ数が格納されることになります。

数値にキャストされるか必要なページ数を取得した後は、paginatorクラスのpageメソッドで指定されたページ番号に対応するページオブジェクトを取得します。
そして最終的に(paginator, page, page.object_list, page.has_other_pages())を返します。
paginatorはクエリセット全体をページネーションするためのオブジェクトです。

paginator.count: #全オブジェクトの総数を取得します。
paginator.num_pages: #全ページ数を取得します。
paginator.page_range: #使用可能なページ番号のリストを取得します。

pageは現在のページに関連する情報やオブジェクトのサブセットへのアクセスを提供します。

page.number: #現在のページ番号を取得します。
page.has_previous(): #前のページが存在する場合はTrueを返します。
page.has_next(): #次のページが存在する場合はTrueを返します。
page.previous_page_number(): #前のページの番号を取得します。
page.next_page_number(): #次のページの番号を取得します。

page.object_listは現在のページに表示されるオブジェクトのサブセットで、ループさせることでページ上の各アイテムを操作または表示します

for item in page.object_list:
    print(item)

page.has_other_pagesはbool型で前または次に他のページが存在する場合はTrueを返し、存在しない場合はFalseを返します。

get_context_dataメソッド

テンプレートに渡されるコンテキストデータを取得するためのメソッドです。

基本的にはビュー内で get_querysetメソッドをオーバーライドすることでリストを操作しますが、例えば、あるビューの結果を基に、その結果を表示するための別のビューにリダイレクトする場合には引数のobject_listを使うことになります。

class MyCustomView(View):
    template_name = 'my_template.html'
    
    def get(self, request, *args, **kwargs):
        # 何らかの処理を行い、オブジェクトリストを生成
        object_list = MyModel.objects.filter(some_field='some_value')
        
        # オブジェクトリストと共に別のビューにリダイレクトする
        return render(request, self.template_name, self.get_context_data(object_list=object_list))
    
    def get_context_data(self, *, object_list=None, **kwargs):
        # object_listが提供されていない場合は、デフォルトのクエリセットを使用
        if object_list is None:
            object_list = MyModel.objects.all()
        
        # コンテキストデータを生成
        context = {
            'object_list': object_list,
            # 追加のコンテキストデータを設定
            'additional_data': 'これは追加のデータです'
        }
        
        return context

上記の場合はobject_listにオブジェクトリストが引数に指定されているので、get_context_data内で指定されたオブジェクトリストをcontextのobject_listキーに追加しています。

get_context_dataメソッドでは最初にqueryset = object_list if object_list is not None else self.object_listでobject_listがあればそれをquerysetにセットし、そうでなければデフォルトのクエリセットをセットします。

page_sizeはget_paginate_byから取得し、値があればpaginate_querysetメソッドの引数にpage_sizeを入れてリストデータをテンプレートで使用するcontextデータに格納します。
値がなければデフォルトのobject_listを返します。
つまり、ページネーションが指定されていれば(ページサイズ)区切り、そうでなければデフォルトのobject_list(queryset)をcontextに入れているということです。

最後にcontext_object_nameが存在すれば、querysetの名前を指定した名前にして、最終的なcontextを親スーパーのget_context_dataにcontextを渡します。

BaseListViewクラス

MultipleObjectMixinViewクラスを継承し、ListViewの基本機能を提供します。
カスタムレスポンスの生成やリスト表示以外の処理を組み込みたい場合にはこのクラスが使用されます。

以下は、BaseListViewを使用して独自のHTTPレスポンスを生成する例です。

class BookListView(BaseListView):
    model = Book

    def get(self, request, *args, **kwargs):
        self.object_list = self.get_queryset()
        context = self.get_context_data()
        # contextを使って独自のHttpResponseを生成
        book_titles = ', '.join([book.title for book in context['object_list']])
        return HttpResponse(f'Books: {book_titles}')

    def get_context_data(self, **kwargs):
        # ベースクラスのコンテキストデータを取得
        context = super().get_context_data(**kwargs)
        # 必要に応じてコンテキストデータをカスタマイズ
        context['extra_data'] = 'This is extra data for the view.'
        return context

getメソッドで取得されたオブジェクトのリストを使用して独自のテキストベースのHTTPレスポンスを生成しています。ビューにアクセスすると、すべての書籍のタイトルがコンマ区切りで表示されるレスポンスが返されます。
独自のレンダリング方法や特定のHTTPレスポンスのフォーマットが必要な場合に有効ですが、HTMLテンプレートを使用して通常のWebページをレンダリングする場合は、ListViewのような派生クラスを使用する方が効率的です。

MultipleObjectTemplateResponseMixinクラス

複数のオブジェクトをテンプレートに渡すために使用され、テンプレート名の生成などを行います。

MultipleObjectTemplateResponseMixinクラス
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
    """Mixin for responding with a template and list of objects."""
    template_name_suffix = '_list'

    def get_template_names(self):
        try:
            names = super().get_template_names()
        except ImproperlyConfigured:
            names = []

        if hasattr(self.object_list, 'model'):
            opts = self.object_list.model._meta
            names.append("%s/%s%s.html" % (opts.app_label, opts.model_name, self.template_name_suffix))
        elif not names:
            raise ImproperlyConfigured(
                "%(cls)s requires either a 'template_name' attribute "
                "or a get_queryset() method that returns a QuerySet." % {
                    'cls': self.__class__.__name__,
                }
            )
        return names

get_template_namesメソッド

まずは継承したTemplateResponseMixinget_template_namesメソッドを使用してnamesを取得します。もしnamesが存在しなければ、空の配列をセットします。

object_listにmodel属性が存在していればそのモデルのメタデータオブジェクトをoptsに格納し、namesにoptsのapp_labelmodel_name、template_name_suffixを使用してtemplate_nameを作成し返却します。

ListViewクラス

上記のMixinとクラスを組み合わせ、リスト表示に関連する一連のタスクを容易にします。
例えば、モデルの指定やテンプレート名、ページネーションの設定などが可能です。

使用例
class BookListView(ListView):
    model = Book  # 表示するモデルを指定
    template_name = 'book_list.html'  # 使用するテンプレートを指定
    context_object_name = 'books'  # テンプレートで使用するコンテキスト変数の名前を指定
    paginate_by = 10  # 1ページに表示するオブジェクトの数を指定
<h1>Book List</h1>
<ul>
    {% for book in books %}
        <li>{{ book.title }} by {{ book.author }}</li>
    {% endfor %}
</ul>

以下のmodelとtemplate_nameを指定するだけで、テンプレートでobject_listを使用することができます。

class IndexListView(ListView):
    model = Item
    template_name = 'pages/index.html'
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2