Django のキャッシュフレームワーク

動的なウェブサイトの基本的なトレードオフは、ええと、それが動的であるということです。ユーザーがページをリクエストするたびに、ウェブサーバーはあらゆる種類の計算を行い (データベースのクエリから、テンプレートのレンダリングやビジネスロジックの実行まで)、サイトの訪問者が目にするページを作成します。これは、処理オーバーヘッドの観点から、ファイルシステムからファイルを読み取る標準的なサーバー構成よりもはるかに高コストです。

ほとんどの Web アプリケーションでは、このオーバーヘッドは大した問題ではありません。ほとんどの Web アプリケーションは washingtonpost.comslashdot.org ではなく、そこそこのトラフィックがある小規模から中規模のサイトです。ただし、中トラフィックから高トラフィックのサイトでは、オーバーヘッドをできる限り削減することが不可欠です。

そこで登場するのがキャッシュです。

何かをキャッシュすることは、次回に計算を実行する必要がないように、高価な計算の結果を保存することです。以下に示すのは、動的に生成された Web ページでキャッシュがどのように機能するかを説明する疑似コードです。

given a URL, try finding that page in the cache
if the page is in the cache:
    return the cached page
else:
    generate the page
    save the generated page in the cache (for next time)
    return the generated page

Django には、動的ページを保存できるロバストなキャッシュ システムが付属しているため、各リクエストごとに動的ページを計算する必要はありません。便宜上、Django は異なるレベルのキャッシュ粒度を提供しています。特定のビューの出力をキャッシュしたり、生成が難しい部分のみをキャッシュしたり、サイト全体をキャッシュできます。

Django は、 Squid やブラウザベースのキャッシュなどの「ダウンストリーム」キャッシュでもうまく機能します。これらは、直接コントロールできないタイプのキャッシュですが、サイトのどの部分をどのようにキャッシュする必要があるかというヒントを (HTTP ヘッダー経由で) 提供できます。

参考

キャッシュフレームワークの設計思想 では、フレームワークの設計上の決定のいくつかについて説明します。

キャッシュのセットアップ

キャッシュシステムにはちょっとした設定が必要です。つまり、キャッシュされたデータをどこに置くか、データベースか、ファイルシステムか、メモリに直接置くかを指定しなければなりません。これはキャッシュのパフォーマンスに影響する重要な決定です。

キャッシュの設定は設定ファイルの CACHES 設定にあります。ここでは、 CACHES で使用可能なすべての値について説明します。

Memcached

Memcached は完全にメモリベースのキャッシュ・サーバで、元々はLiveJournal.comの高負荷を処理するために開発され、その後Danga Interactiveによってオープンソース化されました。FacebookやWikipediaなどのサイトで使用され、データベースへのアクセスを減らし、サイトのパフォーマンスを劇的に向上させます。

Memcached はデーモンとして動作し、指定された量の RAM が割り当てられます。キャッシュにデータを追加、取得、削除するための高速なインターフェイスを提供するだけです。すべてのデータはメモリに直接保存されるので、データベースやファイルシステムの使用によるオーバーヘッドはありません。

Memcached 自体をインストールした後、Memcached バインディングをインストールする 必要があります。Python の Memcached バインディングはいくつかありますが、 Django がサポートしているのは pylibmcpymemcache です。

Django で Memcached を使用する

  • BACKENDdjango.core.cache.backends.memcached.PyMemcacheCache または django.core.cache.backends.memcached.PyLibMCCache (選択した memcached バインディングによる) に設定する
  • LOCATIONip:port の値 (ここで、ip は Memcached デーモンの IP アドレス、port は Memcached が起動しているポート)、または、unix:path の値に設定する (ここで、path は Memcached Unix socket ファイルへのパス)。

次の例では、Memcached は localhost (127.0.0.1) port 11211 で起動していて、pymemcache バインディングを使用しています。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "127.0.0.1:11211",
    }
}

次の例では、Memcached はローカルの Unix socket ファイル /tmp/memcached.sock 経由で利用可能であり、pymemcache バインディングを使用しています。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "unix:/tmp/memcached.sock",
    }
}

Memcached の優れた機能の一つは、複数のサーバでキャッシュを共有できることです。つまり、複数のマシンで Memcached デーモンを実行することができ、各マシンでキャッシュの値を重複させることなく、 単一の キャッシュとして扱うことができます。この機能を利用するには、全てのサーバのアドレスを LOCATION に、セミコロンまたはカンマ区切りの文字列、あるいはリストとして記述します。

このインスタンスでは、IP アドレス 172.19.26.240 と 172.19.26.242 で動作する Memcached インスタンスをポート 11211 で共有します:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": [
            "172.19.26.240:11211",
            "172.19.26.242:11211",
        ],
    }
}

以下の例では、172.19.26.240 (port 11211)、172.19.26.242 (port 11212)、172.19.26.244 (port 11213) の IP アドレスで動作する Memcached インスタンスでキャッシュを共有しています:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": [
            "172.19.26.240:11211",
            "172.19.26.242:11212",
            "172.19.26.244:11213",
        ],
    }
}

デフォルトでは、 PyMemcacheCache バックエンドは以下のオプションを設定します( OPTIONS で上書きできます):

"OPTIONS": {
    "allow_unicode_keys": True,
    "default_noreply": False,
    "serde": pymemcache.serde.pickle_serde,
}

Memcachedについての最後のポイントは、メモリベースのキャッシュには欠点があるということです。キャッシュされたデータはメモリに保存されるので、サーバーがクラッシュするとデータは失われます。メモリは永続的なデータ保存用ではないので、メモリベースのキャッシュを唯一のデータ保存用として使わないようにしましょう。明らかに、 Django のキャッシュバックエンドはどれも、永続的な保存には使うべきではありません。これらは全てキャッシュのためのソリューションであって、ストレージではありません。しかし、メモリベースのキャッシュは特に一時的なものであるため、 ここでに記載しておきます。

Redis

Redis は、キャッシュに利用できるインメモリ データベースです。はじめに、Redis サーバーがローカルまたはリモートマシン上で起動している必要があります。

Redis サーバを設定した後、Redis の Python バインディングをインストールする必要があります。 redis-py は Django によってネイティブにサポートされているバインディングです。 hiredis-py パッケージも推奨のひとつです。

Redis をキャッシュ バックエンドとして Django で使用するには、次のようにします。

  • BACKENDdjango.core.cache.backends.redis.RedisCache に設定する。
  • LOCATION を、適切なスキーマを使用して Redis インスタンスを指す URL に設定する。利用可能なスキーマの詳細 については、redis-py のドキュメントを参照してください。

たとえば、Redis が localhost (127.0.0.1) port 6379 で起動している場合は、次のように設定します。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
    }
}

Redis サーバーは認証で保護されていることがよくあります。ユーザー名とパスワードを提供するには、LOCATION 内に URL とともに追加します。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://username:password@127.0.0.1:6379",
    }
}

レプリケーション・モードで複数のRedisサーバーを設定している場合は、セミコロンまたはカンマ区切りの文字列、あるいはリストとしてサーバーを指定できます。複数のサーバーを使用している場合、書き込み操作は最初のサーバー(リーダー)で実行されます。読み取り操作は、ランダムに選ばれた他のサーバ(レプリカ)で実行されます:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": [
            "redis://127.0.0.1:6379",  # leader
            "redis://127.0.0.1:6378",  # read-replica 1
            "redis://127.0.0.1:6377",  # read-replica 2
        ],
    }
}

データベースのキャッシュ

Django はキャッシュデータをデータベース中に保存できます。この仕組みは、高速で適切にインデックス付けされたデータベースサーバーがある場合に、有効に機能します。

データベースのテーブルをキャッシュのバックエンドに使用する場合には、以下のように設定します。

  • BACKENDdjango.core.cache.backends.db.DatabaseCache に設定する
  • LOCATIONtablename にデータベーステーブルの名前を設定します。この名前は、データベースで未使用かつ有効な名前であれば、自分が好きな名前で構いません。

この例では、キャッシュテーブルの名前を my_cache_table と設定しています。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.db.DatabaseCache",
        "LOCATION": "my_cache_table",
    }
}

他のキャッシュバックエンドとは異なり、データベースキャッシュはデータベースレベルでの期限切れエントリの自動間引き (culling) をサポートしていません。その代わり、 add()set()touch() が呼び出されるたびに、期限切れのキャッシュエントリが「間引き」されます。

キャッシュテーブルを作成する

データベース キャッシュを使用する前に、次のコマンドでキャッシュテーブルを必ず作成する必要があります。

python manage.py createcachetable

このコマンドにより、データベース内に1つのテーブルが作成され、Django のデータベースキャッシュシステムに必要な適切なフォーマットが生成されます。テーブルの名前は LOCATION が使用されます。

複数のデータベースキャッシュが使用される場合、createcachetable は1つのキャッシュごとに、対応する1つのテーブルを作成します。

複数のデータベースを使用する場合、createcachetable は、データベースのルートの allow_migrate() メソッド (以下を参照) を監視します。

migrate と同様に、createcachetable は既に存在する既存のテーブルにたいしては手を触れません。存在しないテーブルのみを生成します。

実行される予定の SQL を実行せずに表示するためには、createcachetable --dry-run オプションを使用します。

複数のデータベース

複数のデータベースでデータベースキャッシュを使用する場合、データベースキャッシュテーブルのためにルーティングの指示も設定する必要があります。ルーティングの目的で、データベースキャッシュテーブルは、django_cache という名前のアプリケーションに CacheEntry という名前のモデルとして表示されます。このモデルはモデルキャッシュには表示されませんが、ルーティングの目的でモデルの詳細情報を使用できます。

たとえば、以下のルーターはすべてのキャッシュの読み込み操作を cache_replica に転送し、すべての書き込み操作を cache_primary に転送します。キャッシュテーブルは cache_primary にだけ同期されます。

class CacheRouter:
    """A router to control all database cache operations"""

    def db_for_read(self, model, **hints):
        "All cache read operations go to the replica"
        if model._meta.app_label == "django_cache":
            return "cache_replica"
        return None

    def db_for_write(self, model, **hints):
        "All cache write operations go to primary"
        if model._meta.app_label == "django_cache":
            return "cache_primary"
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        "Only install the cache model on primary"
        if app_label == "django_cache":
            return db == "cache_primary"
        return None

データベースキャッシュモデルに対してルーティングの方向を指定しなかった場合、キャッシュバックエンドは default データベースを使用します。

そして、データベースキャッシュバックエンドを使用しなかった場合には、データベースキャッシュモデルに対してデータベースルーティングの指示を提供することについて心配する必要はありません。

ファイルシステムのキャッシュ

ファイルベースのバックエンドは、各キャッシュの値を独立したファイルとしてシリアライズして保存します。このバックエンドを使用するには、BACKEND"django.core.cache.backends.filebased.FileBasedCache" に設定して、LOCATION を適切なディレクトリに設定します。たとえば、キャッシュデータを /var/tmp/django_cache に保存するには、次の設定を使ってください。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "/var/tmp/django_cache",
    }
}

Windows では、次のようにドライブ文字をパスの最初に付けてください。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "c:/foo/bar",
    }
}

ディレクトリのパスは絶対パスである必要があります。つまり、ファイルシステムのルートから始まる必要があります。設定の最後にスラッシュを置くかどうかは関係ありません。

この設定が指すディレクトリは、存在して読み込みと書き込みが可能であるか、または、ウェブサーバーを実行しているシステムユーザーによって作成可能であることを確認してください。上の例を続けると、もしサーバーが apache ユーザーで実行されているなら、/var/tmp/django_cache が存在して apache ユーザーにより読み書き可能であるか、このディレクトリを apache ユーザーが作成できることを確認してください。

警告

キャッシュの LOCATION が、MEDIA_ROOTSTATIC_ROOTSTATICFILES_FINDERS のいずれかの配下に含まれていると、機密データが漏洩してしまう可能性があります。

キャッシュファイルへのアクセスを獲得した攻撃者は、HTML コンテンツを改竄できるだけでなく、データが pickle を使用してシリアライズされているため、任意のコードのリモート実行までできてしまいます。

警告

ファイルシステムキャッシュは、大量のファイルを保存すると低速になってしまう可能性があります。この問題に遭遇したときは、別のキャッシュメカニズムの使用を検討してください。FileBasedCache をサブクラス化して、淘汰戦略を改善することもできます。

ローカルメモリのキャッシュ

設定ファイルに他のキャッシュが指定されていない場合、これがデフォルトのキャッシュとなります。インメモリキャッシュの速度の利点が欲しいが、Memcached を扱うのが難しい場合は、 ローカルメモリキャッシュバックエンドを検討してください。このキャッシュはプロセス単位 (後述) で、スレッドセーフです。これを使うには、 BACKEND"django.core.cache.backends.locmem.LocMemCache" に設定します。たとえば

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "unique-snowflake",
    }
}

キャッシュの LOCATION は個々のメモリストアを識別するために使用されます。もし locmem キャッシュが1つしかない場合は LOCATION を省略できます。しかし、複数のローカルメモリーキャッシュがある場合は、それらを区別するために少なくとも1つに名前を割り当てる必要があります。

キャッシュは LRU (least-recently-used) 淘汰戦略 (culling strategy) を使用します。

各プロセスはそれ自身のプライベートキャッシュインスタンスを持つことに注意してください。このことは、ローカル・メモリ・キャッシュが特にメモリ効率に優れているわけではないことを意味します。そのため、おそらく本番環境には良い選択ではありません。開発環境には適しています。

ダミーキャッシュ(開発用)

最後に、 Django には実際にはキャッシュしない「ダミー」キャッシュが付属しています。何もせずにキャッシュインターフェースを実装しているだけです。

これは、様々な場所で重量級のキャッシュを使用する本番サイトと、開発/テスト用に、キャッシュを行わず、コード変更も最小限に留めたい環境がある場合に便利です。ダミーキャッシュをアクティブにするには、BACKEND を以下のように設定します。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.dummy.DummyCache",
    }
}

独自のキャッシュバックエンドを使う

Django は「箱から出してすぐに」多くのキャッシュバックエンドをサポートしていますが、カスタマイズ したキャッシュバックエンドを使いたいこともあるでしょう。Django で外部のキャッシュバックエンドを使用するには、下記のように、 CACHES 設定の BACKEND に Python のインポートパスを使用します:

CACHES = {
    "default": {
        "BACKEND": "path.to.backend",
    }
}

独自のバックエンドを構築する場合は、標準のキャッシュバックエンドをリファレンス実装として使うことができます。コードは Django ソースの django/core/cache/backends/ ディレクトリにあります。

注記:ホストがサポートしていないなど、やむを得ない理由がない限り、Django に付属しているキャッシュバックエンドを使用するべきです。それらは十分にテストされており、よくドキュメント化されています。

キャッシュ引数

それぞれのキャッシュバックエンドはキャッシュの動作を制御するために追加の引数を指定できます。これらの引数は CACHES 設定の追加キーとして提供されます。有効な引数は以下の通りです:

  • TIMEOUT: キャッシュに使用するデフォルトのタイムアウトを秒単位で指定します。この引数のデフォルトは 300 秒(5分)です。 TIMEOUTNone を指定することで、デフォルトではキャッシュキーの有効期限が切れません。値を 0 にすると、キーはすぐに期限切れになります(事実上 "キャッシュしない")。

  • OPTIONS: キャッシュバックエンドに渡すオプション。有効なオプションのリストはバックエンドによって異なり、サードパーティライブラリにバックされたキャッシュバックエンドはそのオプションを直接キャッシュライブラリに渡します。

    独自の淘汰戦略 (culling strategy) を実装しているキャッシュバックエンド (locmemfilesystemdatabase バックエンド) では、以下のオプションが有効です:

    • MAX_ENTRIES: 古い値が削除されるまでにキャッシュに保存できる最大エントリ数。この引数のデフォルトは 300 です。

    • CULL_FREQUENCY: MAX_ENTRIES に達したときに間引き (cull) されるエントリの割合。実際の割合は 1 / CULL_FREQUENCY なので、 CULL_FREQUENCY2 に設定すると、 MAX_ENTRIES に達したときにエントリの半分をカリングします。この引数は整数で、デフォルトは 3 です。

      CULL_FREQUENCY0 という値を指定すると、 MAX_ENTRIES に達したときにキャッシュ全体がダンプされます。一部のバックエンド (特に database) では、キャッシュミスが増える代わりにカリング(間引き)が非常に速くなります。

    Memcached と Redis のバックエンドは、クライアントの動作をより高度に制御できるように、クライアントのコンストラクタにキーワード引数として OPTIONS の内容を渡します。使用例は以下を参照してください。

  • KEY_PREFIX: Django サーバが使用する全てのキャッシュキーに自動的に含まれる文字列です(デフォルトでは先頭に付加されます)。

    詳細は キャッシュのドキュメント を参照してください。

  • VERSION: Django サーバが生成するキャッシュキーのデフォルトのバージョン番号。

    詳細は キャッシュのドキュメント を参照してください。

  • KEY_FUNCTION プレフィックス、バージョン、およびキーを最終的なキャッシュ・キーに合成する方法を定義する関数へのドット区切りパスを含む文字列。

    詳細は キャッシュのドキュメント を参照してください。

この例では、ファイルシステム・バックエンドをタイムアウト60秒、最大容量1000アイテムで設定しています。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
        "LOCATION": "/var/tmp/django_cache",
        "TIMEOUT": 60,
        "OPTIONS": {"MAX_ENTRIES": 1000},
    }
}

以下は、pylibmc ベースのバックエンドをバイナリプロトコル、SASL認証、および ketama 挙動モードを有効に設定する例です。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
        "LOCATION": "127.0.0.1:11211",
        "OPTIONS": {
            "binary": True,
            "username": "user",
            "password": "pass",
            "behaviors": {
                "ketama": True,
            },
        },
    }
}

以下は pymemcache ベースのバックエンドの設定例です。クライアントのプーリングを有効にし (クライアントの接続を維持することでパフォーマンスを向上させることができます)、memcache/ネットワークのエラーをキャッシュミスとして扱い、接続ソケットに TCP_NODELAY フラグを設定します。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
        "LOCATION": "127.0.0.1:11211",
        "OPTIONS": {
            "no_delay": True,
            "ignore_exc": True,
            "max_pool_size": 4,
            "use_pooling": True,
        },
    }
}

以下は、redis ベースのバックエンドの設定例です。データベースに 10 を選択し (デフォルトではRedisには16の論理データベースがあります) 、 parser class を指定し (hiredis-py パッケージがインストールされている場合、デフォルトで redis.connection.HiredisParser が使用されます)、およびカスタムの connection pool class を設定しています (デフォルトでは redis.ConnectionPool が使用されます)。

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379",
        "OPTIONS": {
            "db": "10",
            "parser_class": "redis.connection.PythonParser",
            "pool_class": "redis.BlockingConnectionPool",
        },
    }
}

サイトごとのキャッシュ

キャッシュを設定したら、キャッシュを使う最も簡単な方法はサイト全体をキャッシュすることです。この例のように、 'django.middleware.cache.UpdateCacheMiddleware''django.middleware.cache.FetchFromCacheMiddleware'MIDDLEWARE 設定に追加する必要があります:

MIDDLEWARE = [
    "django.middleware.cache.UpdateCacheMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.cache.FetchFromCacheMiddleware",
]

注釈

いいえ、これは typo ではありません。"update" ミドルウェアはリストの最初に、"fetch" ミドルウェアはリストの最後になければなりません。詳細は少し曖昧ですが、全容を知りたい方は下記の Order of MIDDLEWARE を参照してください。

次に、以下の必要な設定を Django 設定ファイルに追加します:

  • CACHE_MIDDLEWARE_ALIAS -- ストレージに使用するキャッシュのエイリアス。
  • CACHE_MIDDLEWARE_SECONDS -- 各ページがキャッシュされるべき秒数を整数で指定します。
  • CACHE_MIDDLEWARE_KEY_PREFIX -- キャッシュが同じ Django インストールを使っている複数のサイトで共有され ている場合、キーの衝突を防ぐために、サイトの名前か、この Django インスタンスに固有な文字列を指定します。気にしないなら空文字列を使いましょう。

FetchFromCacheMiddleware はリクエストヘッダとレスポンスヘッダが許す限り、ステータス 200 の GET と HEAD レスポンスをキャッシュします。異なるクエリパラメータを持つ同じURLへのリクエストに対するレスポンスはユニークなページとみなされ、別々にキャッシュされます。このミドルウェアは、HEADリクエストが、対応するGETリクエストと同じレスポンスヘッダーで応答されることを期待します。その場合、HEADリクエストに対してキャッシュされたGETレスポンスを返すことができます。

さらに、UpdateCacheMiddlewareダウンストリームキャッシュ に影響を与えるいくつかのヘッダを HttpResponse に自動的に設定します:

  • Expires ヘッダーに現在の日時と定義された CACHE_MIDDLEWARE_SECONDS をセットします。
  • Cache-Control ヘッダにページの最大寿命をセットします -- ここでも CACHE_MIDDLEWARE_SECONDS 設定を使用します。

ミドルウェアについては ミドルウェア (Middleware) を参照してください。

ビューがキャッシュの有効期限を設定している場合 (つまり、ヘッダの Cache-Controlmax-age セクションがある場合) は、 CACHE_MIDDLEWARE_SECONDS ではなく、有効期限までページがキャッシュされます。django.views.decorators.cache のデコレータを使用すると、ビューの有効期限を簡単に設定 (cache_control() デコレータを使用) したり、ビューのキャッシュを無効にしたり (never_cache() デコレータを使用) することができます。これらのデコレータについての詳細は、 他のヘッダを使用する セクションを参照してください。

USE_I18NTrue に設定されている場合、生成されたキャッシュキーにアクティブな 言語 の名前が含まれるようになります。 Django はどうやって言語の優先順位を検出するのか? も参照してください。これにより、自分でキャッシュキーを作成する必要なく、複数言語のサイトを簡単にキャッシュできるようになります。

USE_TZTrue に設定されている場合、キャッシュキーには カレントタイムゾーン も含まれます。

ビューごとのキャッシュ

django.views.decorators.cache.cache_page(timeout, *, cache=None, key_prefix=None)

キャッシュフレームワークをより細かい単位で使用するには、個別のビューの出力をキャッシュする方法があります。django.views.decorators.cachecache_page デコレーターを定義しており、これを使うとビューのレスポンスが自動的にキャッシュされるようになります。

from django.views.decorators.cache import cache_page


@cache_page(60 * 15)
def my_view(request): ...

cache_page はキャッシュのタイムアウトを秒数で指定する1つの引数を取ります。上の例では、my_view() ビューの結果が15分間キャッシュされます。(ここでリーダビリティのために 60 * 15 と書いていることに注意してください。60 * 15900 つまり15分×60秒と評価されます。)

cache_page で設定されたキャッシュのタイムアウトは、Cache-Control ヘッダの max-age ディレクティブよりも優先されます。

サイトごとのキャッシュと同様に、ビューごとのキャッシュは URL でキーが区別されます。もし複数の URL が同じビューを指した場合、各 URL が別にキャッシュされます。my_view の例を続けて、URLconf が次のようになっているとします。

urlpatterns = [
    path("foo/<int:code>/", my_view),
]

このとき、/foo/1//foo/23/ へのリクエストは別々にキャッシュされます。しかし、特定の URL (たとえば、/foo/23/) が一度リクエストされると、その URL への続くリクエストにはキャッシュが使われます。

cache_page はオプション引数の cache を取ることもできます。これを使うと、ビューの結果をキャッシュするときに、デコレータに (CACHES 設定から) 特定のキャッシュを使うように指示できます。デフォルトでは、default キャッシュが使われますが、使いたいキャッシュはどれでも指定できます。

@cache_page(60 * 15, cache="special_cache")
def my_view(request): ...

また、ビューごとにキャッシュプレフィックスを上書きすることもできます。 cache_page はオプションのキーワード引数 key_prefix を取ります。これはミドルウェアの CACHE_MIDDLEWARE_KEY_PREFIX 設定と同じように動作します。 次のように使用します:

@cache_page(60 * 15, key_prefix="site1")
def my_view(request): ...

key_prefix 引数と cache 引数は同時に指定できます。 key_prefix 引数と CACHES で指定した KEY_PREFIX は連結されます。

さらに、 cache_page は自動的にレスポンスに Cache-ControlExpires ヘッダを設定します。このヘッダは ダウンストリームキャッシュ に影響を与えます。

ビューごとのキャッシュを URLconf で指定する

前のセクションの例では、 cache_pagemy_view 関数を変更するため、ビューがキャッシュされることをハードコードしていました。この方法では、ビューをキャッシュシステムに結合することになり、いくつかの理由から理想的ではありません。例えば、キャッシュのない別のサイトでビュー関数を再利用したい場合や、キャッシュされずにビューを使いたい人にビューを配布したい場合などです。このような問題の解決策は、ビュー関数そのものではなく URLconf でビューごとのキャッシュを指定することです。

URLconf でビュー関数を参照するときに cache_page でラップすることで、これを実現できます。以下は以前の古い URLconf です。

urlpatterns = [
    path("foo/<int:code>/", my_view),
]

以下は my_viewcache_page でラップしたものです。

from django.views.decorators.cache import cache_page

urlpatterns = [
    path("foo/<int:code>/", cache_page(60 * 15)(my_view)),
]

テンプレートフラグメントのキャッシュ

さらにコントロールしたい場合は、 cache テンプレートタグを使ってテンプレートの断片をキャッシュすることもできます。テンプレートからこのタグにアクセスするには、テンプレートの先頭付近に {% load cache %} を記述します。

テンプレートタグ {% cache %} は指定した時間だけブロックの内容をキャッシュします。少なくとも2つの引数を取ります。秒単位のキャッシュタイムアウトと、キャッシュフラグメントに与える名前です。timeout が None の場合、フラグメントは永遠にキャッシュされます。名前はそのまま使用されるので、変数は使えません。次に例を示します。

{% load cache %}
{% cache 500 sidebar %}
    .. sidebar ..
{% endcache %}

フラグメントの内部に現れる動的なデータに応じて、フラグメントの複数のコピーをキャッシュしたい場合があります。たとえば、先ほどの例で使用したサイドバーを、サイトのすべてのユーザーに対して個別にキャッシュしたいとします。これを行うには、キャッシュフラグメントを一意に識別するために、フィルタの有無にかかわらず、1つ以上の追加の引数を {% cache %} テンプレートタグに渡します。

{% load cache %}
{% cache 500 sidebar request.user.username %}
    .. sidebar for logged in user ..
{% endcache %}

USE_I18NTrue に設定されている場合、サイトごとのミドルウェアキャッシュは アクティブな言語 を尊重します。テンプレートタグの cache では 翻訳固有の変数 を使えば同じ結果が得られます:

{% load i18n %}
{% load cache %}

{% get_current_language as LANGUAGE_CODE %}

{% cache 600 welcome LANGUAGE_CODE %}
    {% translate "Welcome to example.com" %}
{% endcache %}

テンプレート変数が整数値に解決されるなら、キャッシュタイムアウトにはテンプレート変数が使えます。例えば、テンプレート変数 my_timeout が値 600 に指定されている場合、以下の2つの例は等価です:

{% cache 600 sidebar %} ... {% endcache %}
{% cache my_timeout sidebar %} ... {% endcache %}

この機能はテンプレート内での繰り返しを避けるのに便利です。1つの変数にタイムアウトを指定し、その値を再利用できます。

デフォルトでは、キャッシュタグは "template_fragments" というキャッシュを使用しようとします。そのようなキャッシュが存在しない場合は、デフォルトのキャッシュを使用します。キーワード引数 using で、使用する別のキャッシュバックエンドを選択できます。

{% cache 300 local-thing ...  using="localcache" %}

設定されていないキャッシュ名を指定することはエラーとみなされます。

django.core.cache.utils.make_template_fragment_key(fragment_name, vary_on=None)

キャッシュされたフラグメントに使用されるキャッシュキーを取得したい場合は、 make_template_fragment_key を使用します。フラグメント名 fragment_namecache テンプレートタグの2番目の引数と同じです。この関数は、キャッシュされたアイテムを無効にしたり上書きしたりする場合などに便利です:

>>> from django.core.cache import cache
>>> from django.core.cache.utils import make_template_fragment_key
# cache key for {% cache 500 sidebar username %}
>>> key = make_template_fragment_key("sidebar", [username])
>>> cache.delete(key)  # invalidates cached template fragment
True

低レベルキャッシュ API

レンダリングされたページ全体をキャッシュしてもあまりメリットがないこともありますし、実際、過剰なキャッシュはかえって不便です。

たとえば、あなたのサイトには、結果が複数の高コストなクエリに依存し、その結果が異なる間隔で変化するビューが含まれているとします。この場合、サイト単位やビュー単位のキャッシュ戦略が提供するフルページキャッシュを使うのは理想的ではありません。なぜなら、結果全体をキャッシュしたいとは思わないでしょうが(データの一部が頻繁に変更されるからです)、めったに変更されない結果はキャッシュしたいからです。

このような場合のために、 Django は低レベルのキャッシュ API を公開しています。この API を使えば、好きなレベルでオブジェクトをキャッシュに保存できます。 安全に pickle 化できる Python オブジェクトなら何でもキャッシュできます: 文字列、辞書、モデルオブジェクトのリストなど。(ほとんどの一般的なPythonオブジェクトはpickle化できます。pickle化についての詳細はPythonのドキュメントを参照してください)。

キャッシュへのアクセス

django.core.cache.caches

CACHES 設定で設定したキャッシュには、 django.core.cache.caches という Dict のようなオブジェクトでアクセスできます。同じスレッドで同じエイリアスを繰り返しリクエストすると、同じオブジェクトが返されます。

>>> from django.core.cache import caches
>>> cache1 = caches["myalias"]
>>> cache2 = caches["myalias"]
>>> cache1 is cache2
True

指定したキーが存在しない場合、 InvalidCacheBackendError が発生します。

スレッドセーフを実現するために、キャッシュバックエンドの異なるインスタンスがスレッドごとに返されます。

django.core.cache.cache

ショートカットとして、デフォルトのキャッシュは django.core.cache.cache で利用できます:

>>> from django.core.cache import cache

このオブジェクトは caches['default'] と等価です。

基本的な使用法

基本的なインターフェイスは、次のようになります。

cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)
>>> cache.set("my_key", "hello, world!", 30)
cache.get(key, default=None, version=None)
>>> cache.get("my_key")
'hello, world!'

key には str を指定し、 value には任意の Python オブジェクトを指定します。

引数の timeout はオプションで、デフォルトは CACHES 設定 (上記で説明) にある適切なバックエンドの timeout 引数です。これはキャッシュに保存される秒数です。 timeoutNone を渡すと、値は永遠にキャッシュされます。 timeout0 を渡すと、値はキャッシュされません。

オブジェクトがキャッシュに存在しない場合、 cache.get()None を返します:

>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key")
None

オブジェクトがキャッシュに存在するかどうかを判断する必要があり、リテラル値 None を保存した場合は、デフォルトとして sentinel オブジェクトを使用します:

>>> sentinel = object()
>>> cache.get("my_key", sentinel) is sentinel
False
>>> # Wait 30 seconds for 'my_key' to expire...
>>> cache.get("my_key", sentinel) is sentinel
True

cache.get()default 引数を取ることができます。これは、オブジェクトがキャッシュに存在しない場合に返す値を指定します:

>>> cache.get("my_key", "has expired")
'has expired'
cache.add(key, value, timeout=DEFAULT_TIMEOUT, version=None)

キーがまだ存在しない場合にのみキーを追加するには、 add() メソッドを使用します。このメソッドは set() と同じパラメータを受け取りますが、指定したキーがすでに存在する場合はキャッシュを更新しようとしません:

>>> cache.set("add_key", "Initial value")
>>> cache.add("add_key", "New value")
>>> cache.get("add_key")
'Initial value'

もし add() が値をキャッシュに保存したかどうかを知りたい場合は、戻り値をチェックできます。値が保存されていれば True を返し、そうでなければ False を返します。

cache.get_or_set(key, default, timeout=DEFAULT_TIMEOUT, version=None)

キーの値を取得したり、そのキーがキャッシュにない場合に値を設定したい場合は、 get_or_set() メソッドが使えます。これは get() と同じパラメータを受け取りますが、そのキーのデフォルトの戻り値が設定されます:

>>> cache.get("my_new_key")  # returns None
>>> cache.get_or_set("my_new_key", "my new value", 100)
'my new value'

また、任意の呼び出し可能オブジェクトを デフォルト 値として渡すこともできます:

>>> import datetime
>>> cache.get_or_set("some-timestamp-key", datetime.datetime.now)
datetime.datetime(2014, 12, 11, 0, 15, 49, 457920)
cache.get_many(keys, version=None)

一度だけキャッシュをヒットする get_many() インターフェースもあります。 get_many() は、キャッシュに実際に存在する (そして有効期限が切れていない) キーをすべて含む辞書を返します:

>>> cache.set("a", 1)
>>> cache.set("b", 2)
>>> cache.set("c", 3)
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}
cache.set_many(dict, timeout)

より効率的に複数の値を指定するには、 set_many() を使用してキーと値のペアの辞書を渡します:

>>> cache.set_many({"a": 1, "b": 2, "c": 3})
>>> cache.get_many(["a", "b", "c"])
{'a': 1, 'b': 2, 'c': 3}

cache.set() と同様に、 set_many() はオプションの timeout パラメータを受け取ります。

サポートされているバックエンド (memcached) では、 set_many() は挿入に失敗したキーのリストを返します。

cache.delete(key, version=None)

delete() で明示的にキーを削除することで、特定のオブジェクトのキャッシュをクリアできます:

>>> cache.delete("a")
True

delete() はキーの削除に成功すると True を返し、失敗すると False を返します。

cache.delete_many(keys, version=None)

一度にたくさんのキーをクリアしたい場合は、 delete_many() にクリアしたいキーのリストを渡します:

>>> cache.delete_many(["a", "b", "c"])
cache.clear()

最後に、キャッシュ内のすべてのキーを削除したい場合は cache.clear() を使用します。 clear() はアプリケーションによって設定されたキーだけでなく、 すべての キーをキャッシュから削除します:

>>> cache.clear()
cache.touch(key, timeout=DEFAULT_TIMEOUT, version=None)

cache.touch() はキーの有効期限を設定します。例えば、キーの有効期限を10秒後に更新する場合:

>>> cache.touch("a", 10)
True

他のメソッドと同様に、 timeout 引数はオプションで、デフォルトは CACHES 設定の適切なバックエンドの TIMEOUT オプションです。

touch() はキーが正しく touch された場合は True を返し、そうでない場合は False を返します。

cache.incr(key, delta=1, version=None)
cache.decr(key, delta=1, version=None)

incr() メソッドまたは decr() メソッドを使用して、既に存在するキーをインクリメント/デクリメントすることもできます。デフォルトでは、既存のキャッシュの値が 1 だけインクリメント/デクリメントされます。インクリメント/デクリメントの呼び出しに引数を指定することで、1以外のインクリメント/デクリメント値を指定できます。存在しないキャッシュ・キーをインクリメントまたはデクリメントしようとすると、ValueError が発生します:

>>> cache.set("num", 1)
>>> cache.incr("num")
2
>>> cache.incr("num", 10)
12
>>> cache.decr("num")
11
>>> cache.decr("num", 5)
6

注釈

incr()/decr() メソッドはアトミックであることは保証されていません。アトミックなインクリメント/デクリメントをサポートしているバックエンド (特に memcached バックエンド) では、インクリメントとデクリメントの操作はアトミックに行われます。しかし、バックエンドがネイティブにインクリメント/デクリメント操作を提供していない場合は、取得/更新の2段階を使って実装されます。

cache.close()

キャッシュバックエンドに実装されていれば、 close() でキャッシュへの接続を閉じることができます。

>>> cache.close()

注釈

close メソッドを実装していないキャッシュでは、このメソッドは何もしません。

注釈

メソッドの非同期バージョンは、ベースとなるメソッドに a で始まるプレフィックスが付けられています。例えば、 cache.aadd()cache.adelete_many() です。詳細については Asynchronous support を参照してください。

キャッシュ キーにプレフィックスを付ける

サーバ間でキャッシュインスタンスを共有している場合、あるいは本番環境と開発環境の間でキャッシュインスタンスを共有している場合、あるサーバでキャッシュされたデータが別のサーバで使用される可能性があります。キャッシュされたデータのフォーマットがサーバ間で異なる場合、非常に診断が難しい問題につながる可能性があります。

これを防ぐために、 Django はサーバが使う全てのキャッシュキーの前にプレフィックスを付ける機能を提供します。特定のキャッシュキーが保存または取得されると、 Django は自動的に KEY_PREFIX キャッシュ設定の値をキャッシュキーの先頭に付加します。

各 Django インスタンスに異なる KEY_PREFIX を持たせることで、キャッシュ値が衝突しないようにできます。

キャッシュのバージョン管理

キャッシュされた値を使用する実行中のコードを変更する場合、既存のキャッシュされた値を消去する必要があるかもしれません。これを行う最も簡単な方法はキャッシュ全体を削除することですが、まだ有効で有用なキャッシュ値を失うことになります。

Django は個々のキャッシュ値をターゲットにする、より良い方法を提供します。Django のキャッシュフレームワークには、 VERSION キャッシュ設定で指定できる、システム全体のバージョン識別子があります。この設定の値は、自動的にキャッシュ・プレフィックスとユーザが指定したキャッシュ・キーと組み合わされ、最終的なキャッシュ・キーが得られます。

デフォルトでは、どのようなキャッシュキーのリクエストも自動的にサイトのデフォルトのキャッシュキーのバージョンを含みます。しかし、基本的なキャッシュ関数には version 引数が含まれているので、特定のキャッシュキーのバージョンを指定できます。たとえば:

>>> # Set version 2 of a cache key
>>> cache.set("my_key", "hello world!", version=2)
>>> # Get the default version (assuming version=1)
>>> cache.get("my_key")
None
>>> # Get version 2 of the same key
>>> cache.get("my_key", version=2)
'hello world!'

特定のキーのバージョンは incr_version() メソッドと decr_version() メソッドで増減できます。これにより、特定のキーのバージョンを新しいものに変更できます。先ほどの例の続きで、下記のようにします:

>>> # Increment the version of 'my_key'
>>> cache.incr_version("my_key")
>>> # The default version still isn't available
>>> cache.get("my_key")
None
# Version 2 isn't available, either
>>> cache.get("my_key", version=2)
None
>>> # But version 3 *is* available
>>> cache.get("my_key", version=3)
'hello world!'

キャッシュキーの変換

前の2つのセクションで説明したように、ユーザから提供されたキャッシュ・キーはそのまま使用されるわけではありません。このキーは、キャッシュ・プレフィックスおよびキーバージョンと組み合わされて最終的なキャッシュ・キーとなります。デフォルトでは、3 つの部分はコロンで結合されて最終的な文字列となります:

def make_key(key, key_prefix, version):
    return "%s:%s:%s" % (key_prefix, version, key)

各部分をさまざまな方法で組み合わせたり、最終的なキーに他の処理を施したりしたい場合は(例えば、キーの部品のハッシュダイジェストを取るなど)、独自のキー関数を作成できます。

KEY_FUNCTION キャッシュ設定では、上記の make_key() のプロトタイプにマッチする関数へのドット区切りパスを指定します。指定された場合、デフォルトのキー結合関数の代わりにこのカスタムキー関数が使用されます。

キャッシュキーの警告

最も一般的に使用されているキャッシュバックエンドである Memcached は、 250 文字より長いキャッシュキーや、空白文字や制御文字を含むキャッシュキーを許可しておらず、そのようなキーを使用すると例外が発生します。キャッシュ移植可能なコードを推奨し、不快な驚きを最小化するために、他の組み込みキャッシュバックエンドは、 memcached でエラーを引き起こすようなキーが使用された場合、警告 (django.core.cache.backends.base.CacheKeyWarning) を出します。

より広い範囲のキーを使用できるバックエンド (カスタムバックエンド、または非emcachedの組み込みバックエンド) を使用していて、警告を出さずにより広い範囲のキーを使用したい場合は、 INSTALLED_APPSmanagement モジュールにあるこのコードで CacheKeyWarning を無効にできます:

import warnings

from django.core.cache import CacheKeyWarning

warnings.simplefilter("ignore", CacheKeyWarning)

組み込みのバックエンドに対してカスタムキーバリデーションのロジックを提供したい場合は、そのバックエンドをサブクラス化して validate_key メソッドだけをオーバーライドし、 using a custom cache backend の説明に従ってください。たとえば、 locmem バックエンドに対してこの処理を行うには、次のコードをモジュールに記述します:

from django.core.cache.backends.locmem import LocMemCache


class CustomLocMemCache(LocMemCache):
    def validate_key(self, key):
        """Custom validation, raising exceptions or warnings as needed."""
        ...

...そして、このクラスへのドット区切りパスを CACHES 設定の BACKEND 部分で使用します。

非同期サポート

Django は非同期キャッシュバックエンドのサポートを開発中ですが、非同期キャッシュはまだサポートしていません。将来のリリースで対応する予定です。

django.core.cache.backends.base.BaseCache には 全ての基本メソッド の非同期バージョンがあります。慣習として、すべてのメソッドの非同期バージョンのプレフィックスは a です。デフォルトでは、両方の引数は同じです:

>>> await cache.aset("num", 1)
>>> await cache.ahas_key("num")
True

ダウンストリームキャッシュ

これまで、このドキュメントは自分自身が 持つ データに焦点を当ててきました。しかし、ウェブ開発では別の種類のキャッシュも関係があります。それは、「ダウンストリーム」のキャッシュによって実行されるキャッシングです。ユーザーのために、リクエストがウェブサイトに到達する前にもページをキャッシュするシステムが存在します。

ダウンストリーム キャッシュの例をいくつか挙げます。

  • HTTPを使用する場合、あなたの ISP は特定のページをキャッシュすることがあります。そのため、もしあなたが http://example.com/ からページをリクエストした場合、あなたのISPはexample.comに直接アクセスすることなく、あなたにそのページを送信します。example.comの管理者はこのようなキャッシュを知りません。ISPはexample.comとあなたのウェブブラウザの間に位置し、透過的にすべてのキャッシュを処理します。このようなキャッシュは、中間者攻撃(man-in-the-middle attack)となるため、HTTPSでは不可能です。
  • あなたの Django ウェブサイトは、Squid Web Proxy Cache (http://www.squid-cache.org/) のような プロキシキャッシュ の背後にあるかもしれません。この場合、各リクエストはまずプロキシによって処理され、必要なときだけアプリケーションに渡されます。
  • ウェブブラウザもページをキャッシュします。ウェブページが適切なヘッダーを送信すると、ブラウザはそのページへのその後のリクエストに、変更されたかどうかを確認するためにウェブページに再度問い合わせることなく、ローカルにキャッシュされたコピーを使用します。

ダウンストリーム・キャッシュは効率性を高めてくれますが、危険性もあります。 多くのウェブページのコンテンツは、認証や他の変数のホストに基づいて異なっており、純粋にURLに基づいてやみくもにページを保存するキャッシュシステムは、それらのページにあとから訪問する人に不正確なデータや機密データを公開する可能性があります。

たとえば、ウェブメールシステムを運営している場合、「受信トレイ」ページの内容は、どのユーザーがログインしているかによって異なります。もしISPがやみくもにあなたのサイトをキャッシュしていたら、そのISP経由で最初にログインしたユーザーは、そのユーザー固有の受信トレイページを、それ以降のサイト訪問者のためにキャッシュすることになります。 それはクールではありません。

幸い、HTTP はこの問題の解決方法を提供しています。さまざまな HTTP ヘッダを使用することで、指定した変数に応じてキャッシュ コンテンツを変えるようにダウンストリーム キャッシュに支持したり、特定のページをキャッシュしないようにキャッシュメカニズムに伝えることができます。以下のセクションでは、こうしたヘッダの一部を見ていきます。

Vary ヘッダーを使う

Vary ヘッダは、キャッシュメカニズムがキャッシュキーを作る際に、どのリクエストヘッダを考慮するべきかを定義します。たとえば、ウェブサイトのコンテンツがユーザーの言語設定に依存する場合、ページは「言語によって異なる (vary)」と言うことができます。

デフォルトでは、Django のキャッシュシステムは、リクエストされた完全修飾 URL (たとえば "https://www.example.com/stories/2005/?order_by=author") を使ってキャッシュキーを作成します。つまり、クッキーや 言語設定などのユーザエージェントの違いに関係なく、その URL へのリクエストはすべて同じキャッシュバージョンを使うことになります。しかし、もしこのページがリクエストヘッダの違い、例えばクッキーや言語、ユーザーエージェントの違いによって異なるコンテンツを生成するのであれば、 Vary ヘッダを使ってキャッシュ機構にページの出力がそれらに依存していることを伝える必要があります。

これを Django で行うには、便利な django.views.decorators.vary.vary_on_headers() ビューデコレータを次のように使います:

from django.views.decorators.vary import vary_on_headers


@vary_on_headers("User-Agent")
def my_view(request): ...

この場合、(Django 自身のキャッシュミドルウェアのような) キャッシュ機構は、それぞれのユニークなユーザエージェントに対して、別バージョンのページをキャッシュします。

vary_on_headers デコレータを使用する利点は、手動で Vary ヘッダーを設定する (response.headers['Vary'] = 'user-agent' のように使用する) のではなく、デコレータが Vary ヘッダーに 追加 をする点にあります(これは既に存在するかもしれません)。それにより、最初から設定され、既にそこにあったものを上書きすることを避けることができます。

複数のヘッダを vary_on_headers() に渡すこともできます:

@vary_on_headers("User-Agent", "Cookie")
def my_view(request): ...

これは、下流のキャッシュに対して 両方 の vary に応じるように指示します。これは、user-agent と cookie の各組み合わせがそれぞれ独自のキャッシュ値を持つことを意味します。例えば、user-agent が Mozilla で cookie の値が foo=bar のリクエストは、user-agent が Mozilla で cookie の値が foo=ham のリクエストとは異なるものとみなされます。

クッキーで変化することはとても一般的なので、 django.views.decorators.vary.vary_on_cookie() デコレータがあります。これら2つのビューは等価です:

@vary_on_cookie
def my_view(request): ...


@vary_on_headers("Cookie")
def my_view(request): ...

vary_on_headers に渡すヘッダは大文字小文字を区別しません。 "User-Agent""user-agent" と同じ意味です。

django.utils.cache.patch_vary_headers() というヘルパー関数を直接使うこともできます。この関数は Vary ヘッダ を設定、または追加します。たとえば次のようにします:

from django.shortcuts import render
from django.utils.cache import patch_vary_headers


def my_view(request):
    ...
    response = render(request, "template_name", context)
    patch_vary_headers(response, ["Cookie"])
    return response

patch_vary_headersHttpResponse インスタンスを第 1 引数に、大文字小文字を区別しないヘッダ名のリスト/タプルを第 2 引数にとります。

Vary ヘッダについての詳細は 公式な Vary の仕様 を参照してください。

キャッシュの制御: 他のヘッダを使用する

キャッシュに関するその他の問題点としては、データのプライバシーや、キャッシュの階層構造のどこにデータを保存すべきかという問題があります。

ユーザーは通常2種類のキャッシュに直面します。自分のブラウザのキャッシュ(プライベートキャッシュ)とプロバイダのキャッシュ(パブリックキャッシュ)です。パブリックキャッシュは、複数のユーザーによって使用され、他の誰かによって制御されます。これは、機密データに関する問題を引き起こします。例えば、銀行の口座番号がパブリックキャッシュに保存されることは避けたいものです。そのため、ウェブアプリケーションには、どのデータがプライベートで、どのデータがパブリックであるかをキャッシュに伝える方法が必要です。

解決策は、ページのキャッシュを "private" にすることです。これを Django で行うには、 cache_control() ビューデコレータを使います。例:

from django.views.decorators.cache import cache_control


@cache_control(private=True)
def my_view(request): ...

このデコレータは、適切な HTTP ヘッダーを背後で送信します。

キャッシュ制御の設定 "private" と "public" は互いに排他的であることに注意しましょう。デコレータは、"private" が設定されているときは "public" ディレクティブを削除します (逆も同様です)。2つのディレクティブの使用例は、プライベートとパブリックのエントリを提供するブログサイトです。公開エントリは共有キャッシュにキャッシュされます。以下のコードでは patch_cache_control() を使っています。これはキャッシュコントロールヘッダを手動で変更する方法です (内部的には cache_control() デコレータから呼び出されます):

from django.views.decorators.cache import patch_cache_control
from django.views.decorators.vary import vary_on_cookie


@vary_on_cookie
def list_blog_entries_view(request):
    if request.user.is_anonymous:
        response = render_only_public_entries()
        patch_cache_control(response, public=True)
    else:
        response = render_private_and_public_entries(request.user)
        patch_cache_control(response, private=True)

    return response

ダウンストリームのキャッシュは他の方法でも制御できます (HTTP キャッシュの詳細については RFC 9111 を参照してください)。たとえば、 Django のサーバーサイドキャッシュフレームワークを使わない場合でも、 max-age ディレクティブを使って、ビューを一定時間キャッシュするようにクライアントに指示できます:

from django.views.decorators.cache import cache_control


@cache_control(max_age=3600)
def my_view(request): ...

(キャッシュミドルウェアを使用 する 場合、 CACHE_MIDDLEWARE_SECONDS 設定の値で max-age が既に設定されています。その場合、 cache_control() デコレータで指定した max_age が優先され、ヘッダ値は正しくマージされます)。

有効な Cache-Control レスポンスディレクティブはすべて cache_control() で有効です。以下はその例です:

  • no_transform=True
  • must_revalidate=True
  • stale_while_revalidate=num_seconds
  • no_cache=True

既知のディレクティブの全リストは IANA registry にあります (すべてのディレクティブがレスポンスに適用されるわけではないことに注意してください)。

ヘッダーを使用してキャッシュを完全に無効にしたい場合は、 never_cache() が使えます。これはビューのデコレータであり、レスポンスがブラウザーや他のキャッシュによってキャッシュされないようにするヘッダーを追加します。例:

from django.views.decorators.cache import never_cache


@never_cache
def myview(request): ...

MIDDLEWARE の順序

キャッシュミドルウェアを使用する場合、下記の2つを MIDDLEWARE 設定の適切な場所に配置することが重要です。キャッシュミドルウェアはどのヘッダによってキャッシュストレージを変化させるかを知る必要があるからです。ミドルウェアは常に Vary レスポンスヘッダに何かを追加します。

UpdateCacheMiddleware はレスポンスフェーズの間に実行され、ミドルウェアは逆順に実行されるため、リストの先頭にある項目はレスポンスフェーズの間、 最後に 実行されます。したがって、 UpdateCacheMiddlewareVary ヘッダに何かを追加する可能性のある他のミドルウェアの 前に 配置されていることを確認する必要があります。以下のミドルウェアモジュールは Vary ヘッダに何らかのものを追加します:

  • SessionMiddlewareCookie を追加します
  • GZipMiddlewareAccept-Encoding を追加します
  • LocaleMiddlewareAccept-Language を追加します

一方、FetchFromCacheMiddleware はリクエストフェーズで実行され、このフェーズではミドルウェアが先頭から最後にかけて適用されるため、リストの上部にあるアイテムはリクエストフェーズの 最初 に実行されます。 FetchFromCacheMiddleware もまた、他のミドルウェアが Vary ヘッダーを更新した後に実行する必要があるので、 FetchFromCacheMiddlewareVary ヘッダーを更新するようなアイテムの になければなりません。

Back to Top