【ITニュース解説】Django's Four Levels of Caching, and How to Choose
2026年10月23日に「Dev.to」が公開したITニュース「Django's Four Levels of Caching, and How to Choose」について初心者にもわかりやすく解説しています。
ITニュース概要
Djangoではアプリの高速化のため、4段階のキャッシュ機能を使う。サイト全体から特定データまで対象範囲に応じた使い分けが重要だ。ユーザー情報の漏洩を防ぎつつ、性能を最適化するには、適切なレベル選択と設定が必須となる。
ITニュース解説
WebサイトやWebアプリケーションを開発する上で、ユーザーが快適にサービスを利用できるように、表示速度を速めることは非常に重要だ。特に、多くの情報を扱うサービスや、人気のあるサービスでは、アクセスが集中するとページの読み込みが遅くなったり、最悪の場合はサービスが停止してしまったりすることもある。このような問題を解決するための強力な技術の一つが「キャッシュ」である。キャッシュとは、一度計算したり取得したりしたデータを一時的に保存しておき、次に同じデータが必要になったときに、再度計算し直したり取得し直したりする手間を省いて、保存しておいたデータをすぐに返す仕組みのことを指す。これにより、データベースへのアクセス回数を減らしたり、複雑な処理を何度も繰り返すのを避けたりできるため、Webページの表示速度が大幅に向上する。DjangoというWebフレームワークでは、このキャッシュ機能が非常に強力かつ柔軟に提供されており、サイト全体から特定のデータまで、さまざまな粒度でキャッシュを適用できる。しかし、ただキャッシュを使えば良いというものではなく、どのレベルでキャッシュを適用するかが非常に重要だ。間違った使い方をすると、かえって問題を引き起こすこともある。この記事では、Djangoが提供する4つのキャッシュレベルについて、それぞれの特徴と、どのような場合にどのレベルを選ぶべきか、そして安全にキャッシュを利用するための注意点を解説する。システムエンジニアを目指す皆さんにとって、パフォーマンス改善の重要な武器となるはずだ。
キャッシュを利用し始める前に、まずキャッシュされたデータをどこに保存するかをDjangoに教えてあげる必要がある。これを「キャッシュバックエンド」の設定と呼ぶ。本番環境でよく使われるキャッシュバックエンドの一つに「Redis」がある。Redisはメモリ上にデータを保存するため非常に高速で、複数のアプリケーションサーバー間でキャッシュデータを共有できるという利点がある。Django 6.x以降では、このRedisが標準でサポートされるようになったため、追加のパッケージをインストールすることなく利用できる。設定ファイル(settings.py)で、利用するキャッシュバックエンドの種類と、Redisサーバーの場所、キャッシュデータの有効期限などを指定する。例えば、以下のような設定になるだろう。
1CACHES = { 2 "default": { 3 "BACKEND": "django.core.cache.backends.redis.RedisCache", 4 "LOCATION": "redis://127.0.0.1:6379/1", 5 "KEY_PREFIX": "myapp", 6 "TIMEOUT": 300, # 5分 (300秒) 7 } 8}
ここで重要な原則として、キャッシュは一時的なデータを保存する場所であり、決して永続的なストレージとして使うべきではないという点がある。キャッシュに保存されたデータは、いつでも削除される可能性がある。そのため、もし失われても困らない、いつでも再計算や再取得が可能なデータだけをキャッシュに保存する。キャッシュはあくまで速度向上のための層であり、データベースの代わりにはならない。この原則を理解した上で、次にDjangoの4つのキャッシュレベルを見ていこう。
最初のレベルは、Webサイト全体のすべてのページをキャッシュする方法だ。これは最も広範囲にキャッシュを適用するアプローチで、Djangoのミドルウェアと呼ばれる仕組みを利用する。具体的には、UpdateCacheMiddlewareとFetchFromCacheMiddlewareという2つのミドルウェアをsettings.pyのMIDDLEWARE設定に追加する。UpdateCacheMiddlewareはリクエスト処理の最初に、FetchFromCacheMiddlewareは最後に配置することが重要だ。これにより、Webサイトへのリクエストが来たら、まずキャッシュにデータがあるかを確認し、あればそのデータを返す。なければ通常の処理を行い、その結果をキャッシュに保存してからユーザーに返す、という流れがサイト全体に適用される。例えば、CACHE_MIDDLEWARE_SECONDS = 600と設定すれば、すべてのページが10分間キャッシュされることになる。このレベルのキャッシュが適しているのは、ほとんど内容が変わらず、すべての訪問者に対して同じ内容が表示される静的な情報が中心のサイトだ。しかし、ログイン機能があったり、ユーザーごとに表示内容が変わるパーソナライズされたページがあるサイトでは、この方法は非常に危険だ。あるユーザーがログインした状態のページがキャッシュされてしまい、次に訪れた別のユーザーにもそのキャッシュされたページが誤って表示されてしまう可能性があるため、多くの実際のアプリケーションではこのレベルのキャッシュは利用しない。
次に、より粒度を下げて、特定のWebページ(Djangoでは「ビュー」と呼ぶ)の出力をキャッシュする方法がある。これが最も一般的に知られているキャッシュ方法で、@cache_pageというデコレータをビュー関数に適用することで実現できる。例えば、以下のように記述する。
1from django.views.decorators.cache import cache_page 2 3@cache_page(60 * 15) # このビューの応答を15分間キャッシュする 4def product_list(request): 5 products = Product.objects.all() # 高コストなデータベースクエリ 6 return render(request, "products.html", {"products": products})
この例では、product_listという商品一覧を表示するビューが15分間キャッシュされる。最初のアクセスでは、ビューの処理が実行され、データベースから商品データを取得し、テンプレートをレンダリングし、その結果がキャッシュに保存される。その後の15分間は、同じURLへのリクエストが来ても、ビューの処理やデータベースへのアクセス、テンプレートのレンダリングは行われず、キャッシュに保存された応答が即座に返される。これは、読み込みが中心で、内容の変更が少なく、すべてのユーザーに同じ内容が表示されるようなページにとって、非常に効果的で簡単なパフォーマンス改善策となる。しかし、この@cache_pageデコレータには重要な注意点がある。それは、キャッシュのキーが「URL」にのみ基づいて生成されるということだ。つまり、「/products/というURLへの応答」がキャッシュされ、このURLにアクセスするすべてのユーザーに同じキャッシュデータが返される。これは公開されている商品一覧ページでは問題ないが、もしこれがユーザーごとのダッシュボードページだった場合、最初にアクセスしたユーザーのダッシュボードがキャッシュされてしまい、次にアクセスした別のユーザーにもその最初のユーザーのダッシュボードが表示されてしまうという問題が発生する。この問題を解決するために、Djangoには「Varyヘッダー」という仕組みが用意されている。Varyヘッダーは、キャッシュに対して「このページの応答は、URLだけでなく、このリクエストヘッダーの値によっても変わるから、それもキャッシュキーに含めてね」と伝える役割を持つ。@vary_on_headersや@vary_on_cookieといったデコレータを利用する。例えば、言語によって表示が変わるページであれば@vary_on_headers("Accept-Language")を@cache_pageと合わせて使うことで、英語のページとフランス語のページがそれぞれ別々にキャッシュされるようになる。また、ユーザーごとに内容が変わるページの場合は@vary_on_cookieを使うことで、セッションクッキーに基づいて各ユーザーごとに異なるキャッシュエントリが作成される。これにより、ユーザーのデータが誤って他のユーザーに表示されることを防ぐことができる。
Webページの中には、ほとんどの部分は常に動的に変化するが、一部だけは内容が変わらず、かつその部分の生成に大きなコストがかかる、といったケースがある。例えば、ユーザーごとに表示内容が異なるダッシュボードページの中に、「人気のカテゴリー」のような、すべてのユーザーに共通で、かつデータベースから複雑なデータを取得して表示するようなセクションがある場合だ。このような場合、ページ全体をキャッシュしてしまうとパーソナライズされた部分が問題になるが、かといってその高コストな部分を毎回計算し直すのは無駄だ。このような状況に最適なのが、「テンプレートフラグメントキャッシュ」である。これは、Djangoのテンプレート内で{% cache %}タグを使って、テンプレートの一部分だけをキャッシュする仕組みだ。
1{% load cache %} 2 3{# このブロックを1800秒(30分)間、"sidebar"という名前でキャッシュする #} 4{% cache 1800 sidebar %} 5 <ul> 6 {% for category in expensive_category_list %} 7 <li>{{ category.name }}</li> 8 {% endfor %} 9 </ul> 10{% endcache %}
この例では、{% cache %}と{% endcache %}で囲まれた部分だけが30分間キャッシュされる。このブロックの外側にあるコンテンツは、リクエストごとに毎回新しくレンダリングされる。これにより、ページの大部分は動的に保ちながら、高コストで安定している部分だけを効率的にキャッシュできる。また、このテンプレートフラグメントキャッシュも、特定の条件に応じてキャッシュ内容を変化させることが可能だ。{% cache 1800 sidebar request.user.id %}のように、キャッシュタグに変数(例: request.user.id)を追加することで、ユーザーごとに異なる「sidebar」のキャッシュエントリを作成できる。これは、パーソナライズされたページの中にある、部分的に安定している要素をキャッシュする際に非常に役立つ機能だ。
最後に、最も細かい粒度で、そして最も柔軟にキャッシュを制御できるのが「低レベルキャッシュAPI」だ。これは、ビューやテンプレートといった特定のDjangoの機能に縛られず、Pythonの任意のデータを直接キャッシュに保存したり、キャッシュから取得したりする仕組みである。開発者が明示的にキャッシュのキーを決定し、データの保存、取得、そして削除(無効化)をプログラム的に制御できる。
1from django.core.cache import cache 2 3def get_dashboard_stats(user): 4 key = f"dashboard_stats:{user.id}" # 自分でキャッシュキーを決定 5 stats = cache.get(key) # まずキャッシュからデータを取得してみる 6 if stats None: # キャッシュにデータがなければ(キャッシュミス)計算する 7 stats = expensive_stats_calculation(user) # 高コストな計算を実行 8 cache.set(key, stats, 60 * 10) # 計算結果を10分間キャッシュに保存 9 return stats
この例では、get_dashboard_statsという関数が高コストな統計情報を計算すると仮定している。関数が呼び出されると、まずdashboard_stats:ユーザーIDというキーでキャッシュにデータがあるかを調べる。もしキャッシュにデータがあれば、それをすぐに返す。データがなければ、高コストなexpensive_stats_calculationを実行し、その結果を10分間キャッシュに保存してから返す。この低レベルAPIの最大の利点は、その高い制御性にある。開発者は「何をキャッシュするか」「どのようなキーでキャッシュするか」「どれくらいの期間キャッシュするか」そして「いつキャッシュを削除(無効化)するか」をすべて自由に決定できる。特に、パーソナライズされた複雑なアプリケーションでは、この低レベルAPIがキャッシュ戦略の中心となることが多い。
Djangoの4つのキャッシュレベルを理解した上で、最も重要なのは、それぞれの状況に合わせて最適なレベルを選択することだ。これは、アプリケーションのパフォーマンスを安全かつ効果的に向上させるためのスキルと言える。まず最初に、ウェブページ全体がすべてのユーザーに対して同じ内容を表示し、かつそのページの生成に時間やコストがかかる場合では、「ビューごとのキャッシュ(Per-View Caching)」である@cache_pageを試してみるのが最も手軽で大きな効果を期待できる。ただし、ページの出力が言語やログイン状態、ユーザー情報によって変わる場合は、Varyデコレータを必ず追加して、キャッシュキーにこれらの要素を含めることを忘れてはならない。次に、ページ全体は動的だが、そのページの一部に、あまり変化しないが高コストなコンテンツがある場合は、「テンプレートフラグメントキャッシュ(Template Fragment Caching)」が適している。そして、特定の計算結果やデータベースクエリの結果、外部APIの応答など、より具体的な「データ」をキャッシュしたい場合、あるいはキャッシュのキーや無効化のタイミングをきめ細かく制御したい場合は、「低レベルキャッシュAPI(Low-Level Cache API)」を利用する。最後に、「サイト全体のキャッシュ(Per-Site Caching)」は、ユーザーのログインが一切なく、完全に静的なコンテンツで構成されるサイトにのみ適用を検討する。それ以外の場合では、ユーザー間でデータが混在するリスクが非常に高いため、通常は選択すべきではない。このように、何が高コストで、その情報がどの範囲で安定しているのかというスコープに合わせて、適切なキャッシュレベルを選ぶことが、パフォーマンス向上と安全性の両方を実現するための鍵となる。
どのレベルのキャッシュを利用するにしても、以下の二つの原則を守ることで、キャッシュにまつわる一般的なトラブルを防ぐことができる。一つ目は、ユーザーごとに異なるデータをキャッシュする場合、キーにユーザー情報を必ず含めることだ。もしキャッシュするデータがユーザーごとに異なるのであれば、その違いを示す情報(ユーザーID、セッションID、言語コードなど)をキャッシュキーに含める必要がある。「Vary」ヘッダーを使うか、テンプレートフラグメントのキー変数を使うか、低レベルAPIで自分でキーを設計する際にユーザーIDを含めるかなど、適切な方法でこの原則を徹底する。これを怠ると、情報漏洩や誤った情報表示という重大なバグにつながる可能性がある。二つ目は、有効期限(TTL)だけに頼らず、変更時に能動的にキャッシュを無効化する計画を持つことだ。キャッシュの有効期限は、「最大でN秒間は古いデータを表示しても良い」という意味に過ぎない。常に最新の正確な情報が表示されなければならないデータの場合、有効期限切れを待つだけでは危険だ。このようなデータがデータベースで更新された際には、プログラムから明示的にcache.delete(key)を呼び出して、該当するキャッシュエントリを削除する必要がある。有効期限は、あくまでデータが無効化されなかった場合の「安全網」として設定し、基本的にはデータが変更された際に能動的にキャッシュを無効化する仕組みを構築することが重要だ。これらの原則を遵守することで、キャッシュを強力なパフォーマンス改善ツールとして、安全に、そして効果的に活用できるようになるだろう。
Djangoのキャッシュ機構は、サイト全体から個別のデータまで、幅広いニーズに対応できる4つのレベルで提供されている。バックエンドの設定は一度行い、Redisが標準で利用可能になったため、本番環境での利用も容易だ。キャッシュは一時的なデータのためのものであり、永続ストレージではないという基本原則を常に意識することが重要だ。どのレベルを選ぶかは、何が高コストで、そのデータがどの範囲で、どれくらいの期間安定しているかという「スコープ」を正確に把握することにかかっている。そして、ユーザーデータに関する情報漏洩を防ぎ、常に最新の情報を表示するためには、「パーソナライズされたデータはキーに含める」ことと「変更時には能動的にキャッシュを無効化する」という二つの安全ルールを忘れてはならない。これらの知識を身につけることで、アプリケーションのボトルネックを特定し、適切なキャッシュ戦略を立てられるようになる。それは、Webアプリケーションのパフォーマンスを劇的に向上させ、ユーザーエクスペリエンスを高めるための、システムエンジニアとしての重要なスキルとなるだろう。