組み込みページネーション#

これはDjango Ninjaシリーズチュートリアルの第24回です。
ページネーション(pagination)機能は、データ量が少ない小規模なプロジェクトであっても、非常に重要です。
ページネーションがなくてもAPIは動作しますが、特にデータ量が多い場合にはパフォーマンスに影響が出ます。
APIが一度に大量のデータを返すと、サーバーへの負荷が増加するだけでなく、クライアント側の処理が遅くなり、タイムアウトやメモリ不足などの問題を引き起こす可能性さえあります。
ページネーションを通じて、一度に大量のデータを転送することを避け、APIのパフォーマンスを向上させると同時に、ユーザー体験を向上させることができます。
このテーマは前編・後編に分け、Django Ninjaでページネーション機能を実装する方法を紹介します。さまざまなニーズに応えるため、組み込みのページネーションからカスタムページネーションクラスまで解説します。
本文中のすべてのコード変更は、こちらのPRを参照してください。
GitHubサンプルプロジェクト#
ページネーションの重要性#
ページネーションの中心的な役割は、大量のデータを小さな部分に分割して転送し、毎回少しずつ渡すことでパフォーマンスの問題を回避することです。
具体的には、ページネーションは以下の点で役立ちます:
- サーバーの負荷を軽減:一度にすべてのデータを返す必要はなく、単一ページのデータのみを処理します。
- ネットワーク転送速度を向上:大量のデータを転送すると、ネットワークの遅延やデータ損失のリスクが高まります。ページネーションにより、転送の需要を効果的に減らすことができます。
- ユーザー体験を向上:クライアントはすべてのデータの転送完了を待つことなく、初期データを素早く取得して表示できます。同時に、ページネーションはクライアントが大量のデータを処理する負荷を軽減することもできます。
したがって、プロジェクトの規模に関わらず、効率的なページネーション戦略を実装することは、APIの拡張性とユーザー体験に対して明確な助けとなります。
ページネーションの重要性を理解したところで、実装を始めましょう!
今回のサンプルAPIは「記事リストの取得」です。
プロジェクトのデータベースにはすでに60件以上の記事データがあるため、デモンストレーションに最適です。
え?持っていない?それなら〈第12回:リクエスト(四)リクエストボディとスキーマの紹介〉の最後にある「休憩と準備」のセクションを参考にしてください。
今回はDjango Ninja組み込みのPageNumberPaginationを使用して、シンプルで効果的なページネーション機能を実装します。カスタム部分については次回に回します。
Django Ninja組み込みのページネーション#
Django Ninjaでは、組み込みのpaginateデコレータとページネーションクラスを追加することでページネーション機能を実現できます。
Django Ninjaには2つの組み込みページネーターがあります。それぞれの違いを簡単に説明します:
LimitOffsetPagination:「どのデータから始めるか」と「どれだけのデータを取得するか」に基づいてページネーションを行います。データ量が非常に多い場合に適しています。例:「20件目から始めて、10件のデータを取得する」。PageNumberPagination:ページ番号を通じてページネーションを行います。ユーザーは「2ページ目のデータを取得する」のように希望のページ数を指定するだけで済みます。各ページのデータ数は開発者が自由に設定できます。
私は個人的にPageNumberPaginationを使用するか、似たようなカスタムバージョン(次回の内容)を作成することを好みます。
ただし、デフォルトのページネーションはLimitOffsetPaginationであるため、paginateデコレータを使用する際には、第一引数を明示的に宣言する必要があります。すぐに分かりますよ。
その前に、「記事リストの取得」APIの現状を振り返ってみましょう。
APIの現状#
以下に示すように、まだページネーション機能が実装されていないため、一度にすべての記事データを返してしまいます:
@router.get('/posts/', response=list[PostListResponse], ...)
def get_posts(
request: HttpRequest,
title: None | str = Query(None, min_length=2, max_length=10),
) -> QuerySet[Post]:
"""
記事リストの取得
"""
posts = Post.objects.all()
if title:
posts = posts.filter(
title__icontains=title).select_related('author')
return posts
これは記事が少ないうちは正常に動作しますが、データ量が増えるにつれてパフォーマンスに影響が出ます。
次に、Django Ninja組み込みのページネーションを使用してこの問題を改善します。
PageNumberPaginationによるページネーション実装#
組み込みのPageNumberPaginationを使用すると、APIに簡単にページネーション機能を追加できます。
view関数に@paginateデコレータを使用してパラメータを追加するだけです:
from ninja.pagination import PageNumberPagination, paginate
...
@router.get(...)
@paginate(PageNumberPagination, page_size=10) # ページネーション実装
def get_posts(...) -> QuerySet[Post]:
"""
記事リストの取得
"""
...
大部分は省略しましたが、ここではページネーションの実装にのみ注目してください。
前述の通り、第一引数のPageNumberPaginationは明示的に宣言する必要があります。LimitOffsetPaginationを使用する場合は不要です。
実装の結果、各ページに10件の記事が表示されるようになります。この数はpage_sizeパラメータで制御できます。
おや、ページ切り替えはどうすればいいのでしょうか?PageNumberPaginationのソースコードを見てみましょう:
class PageNumberPagination(AsyncPaginationBase):
class Input(Schema):
page: int = Field(1, ge=1)
...
Inputはリクエストのクエリパラメータ(次回詳しく説明します)を表します。言い換えれば、URLのクエリパラメータ(query parameters)でpageパラメータを使用して「ページ番号」を指定することで、ページの切り替えを実現できます。
ページネーションの効果をテストする#
APIを呼び出し、クエリパラメータとして?page=2を使用し、効果を確認してみましょう:

期待通りです!
レスポンスには確かに2ページ目の内容が表示されています——記事のidは11から始まり、合計10件です。
組み込みページネーションのメリットと制限#
メリット#
- 簡単なページネーションデコレータ + 組み込みページネーションを使用するだけで実現でき、各ページの件数を制御できるため、非常に便利で実用的です。
- 複雑なカスタマイズが不要な状況に適しています。
制限#
- 柔軟性に欠けます。例えば、ユーザーに各ページの表示件数を指定させることができません。
- レスポンスのフィールドやフォーマットが固定されており——少し簡素です。
まとめ#
Django Ninja組み込みのページネーションを使用することで、APIに素早くページネーション機能を追加し、シンプルなページネーションの要件をすぐに実現できます。
しかし、組み込みページネーションは制御の柔軟性に欠け、カスタマイズされたレスポンスを作成することもできません。ページネーションの要件が複雑になると、少し力不足に感じます。
この場合、カスタムページネーションがより良いソリューションになります。
次回は、これらの高度な要件を満たすために、Django Ninjaでカスタムページネーションクラスを作成する方法を探ります。