Django Cache

PythonDjangoライブラリキャッシュWebフレームワークパフォーマンス

GitHub概要

django/django

The Web framework for perfectionists with deadlines.

スター85,495
ウォッチ2,277
フォーク33,123
作成日:2012年4月28日
言語:Python
ライセンス:BSD 3-Clause "New" or "Revised" License

トピックス

appsdjangoframeworkmodelsormpythontemplatesviewsweb

スター履歴

django/django Star History
データ取得日時: 2025/10/22 10:04

ライブラリ

Django Cache

概要

Django Cacheは、Djangoフレームワークに組み込まれた包括的なキャッシングシステムです。複数のキャッシュバックエンドをサポートし、サイト全体、ビュー単位、テンプレートフラグメント単位でのキャッシング機能を提供します。

詳細

Django Cacheは、動的Webアプリケーションのパフォーマンスを大幅に向上させるために設計された、柔軟で強力なキャッシングフレームワークです。主要なキャッシュバックエンドとして、Local Memory(locmem)、Database、File-based、Memcached(PyMemcache、PyLibMC)、Redis、Dummyキャッシュをサポートしています。3つのレベルのキャッシングが可能で、ページ全体をキャッシュするサイト全体キャッシュ、個別のビュー出力をキャッシュするビューレベルキャッシュ、テンプレートの特定部分をキャッシュするテンプレートフラグメントキャッシュがあります。cache_page装飾子、vary_on_headers、cache_control装飾子など、豊富な装飾子により細かい制御が可能です。キャッシュキーの生成、バージョニング、タイムアウト設定、キャッシュ無効化など、エンタープライズレベルの機能を提供します。非同期APIも導入され、async/awaitパターンでの使用もサポートしています。

メリット・デメリット

メリット

  • フレームワーク統合: Djangoに完全統合されており、設定と使用が簡単
  • 複数バックエンド: Memcached、Redis、データベース、ファイルベースなど豊富な選択肢
  • 柔軟なキャッシュレベル: サイト全体、ビュー、テンプレートフラグメント単位での制御
  • 豊富な装飾子: cache_page、vary_on_headers、never_cacheなどの便利な装飾子
  • キャッシュキー管理: バージョニング、プレフィックス、カスタムキー生成をサポート
  • 非同期サポート: async/awaitパターンでのキャッシュ操作が可能
  • 開発環境サポート: DummyキャッシュやLocal Memoryで開発時の利便性を提供

デメリット

  • 設定の複雑さ: 大規模プロジェクトでは複数キャッシュの設定が複雑になる場合がある
  • バックエンド依存: 選択したバックエンドの制限や特性に依存する
  • デバッグの困難さ: キャッシュが有効な状態でのデバッグが困難になる場合がある
  • メモリ使用量: Local Memoryキャッシュはプロセス固有でメモリ効率が悪い
  • ファイルベースの性能: File-basedキャッシュは書き込み操作で性能低下が顕著
  • キーの制限: Memcachedなど一部バックエンドではキーの文字数や文字種に制限がある

主要リンク

書き方の例

基本的なキャッシュ設定

# settings.py
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379',
        'OPTIONS': {
            'db': '1',
        },
        'TIMEOUT': 300,  # 5分のデフォルトタイムアウト
        'KEY_PREFIX': 'myapp',
        'VERSION': 1,
    },
    'sessions': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
        'TIMEOUT': 86400,  # 24時間
    },
    'localcache': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
        'TIMEOUT': 300,
        'OPTIONS': {
            'MAX_ENTRIES': 1000,
        }
    }
}

# キャッシュミドルウェアの設定(サイト全体キャッシュ)
MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
]

CACHE_MIDDLEWARE_ALIAS = 'default'
CACHE_MIDDLEWARE_SECONDS = 600
CACHE_MIDDLEWARE_KEY_PREFIX = 'site'

低レベルキャッシュAPI

from django.core.cache import cache, caches

# デフォルトキャッシュの使用
def get_user_profile(user_id):
    cache_key = f'user_profile_{user_id}'
    
    # キャッシュから取得を試行
    profile = cache.get(cache_key)
    if profile is None:
        # キャッシュミスの場合、データベースから取得
        profile = UserProfile.objects.get(user_id=user_id)
        # キャッシュに保存(1時間)
        cache.set(cache_key, profile, 3600)
    
    return profile

# 複数キーの操作
def get_multiple_users(user_ids):
    cache_keys = [f'user_{uid}' for uid in user_ids]
    
    # 複数のキーを一度に取得
    cached_users = cache.get_many(cache_keys)
    
    # キャッシュにないユーザーを特定
    missing_ids = []
    for uid in user_ids:
        if f'user_{uid}' not in cached_users:
            missing_ids.append(uid)
    
    # 不足分をデータベースから取得
    if missing_ids:
        fresh_users = User.objects.filter(id__in=missing_ids)
        fresh_data = {f'user_{u.id}': u for u in fresh_users}
        
        # キャッシュに保存
        cache.set_many(fresh_data, timeout=1800)
        cached_users.update(fresh_data)
    
    return list(cached_users.values())

# 特定のキャッシュバックエンドを使用
session_cache = caches['sessions']
session_cache.set('session_data', session_info, 86400)

ビューレベルキャッシュ

from django.views.decorators.cache import cache_page, never_cache
from django.views.decorators.vary import vary_on_headers, vary_on_cookie

# ページ全体を15分間キャッシュ
@cache_page(60 * 15)
def product_list(request):
    products = Product.objects.filter(is_active=True)
    return render(request, 'products/list.html', {'products': products})

# 特定のキャッシュバックエンドを使用
@cache_page(60 * 30, cache='localcache')
def heavy_computation_view(request):
    result = perform_heavy_computation()
    return JsonResponse({'result': result})

# ヘッダーに基づくキャッシュの分離
@vary_on_headers('User-Agent', 'Accept-Language')
@cache_page(60 * 60)
def responsive_view(request):
    return render(request, 'responsive.html')

# Cookieに基づくキャッシュの分離(ユーザー固有コンテンツ)
@vary_on_cookie
@cache_page(60 * 10)
def user_dashboard(request):
    if request.user.is_authenticated:
        data = get_user_specific_data(request.user)
        return render(request, 'dashboard.html', {'data': data})
    else:
        return redirect('login')

# キャッシュを無効化
@never_cache
def real_time_data(request):
    current_time = timezone.now()
    return JsonResponse({'timestamp': current_time.isoformat()})

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

<!-- templates/product_detail.html -->
{% load cache %}

<div class="product-detail">
    <h1>{{ product.name }}</h1>
    
    <!-- 商品説明を30分間キャッシュ -->
    {% cache 1800 product_description product.id %}
        <div class="description">
            {{ product.description|markdown }}
        </div>
    {% endcache %}
    
    <!-- ユーザー固有のレビューセクション -->
    {% cache 600 user_reviews product.id user.id %}
        <div class="user-reviews">
            {% for review in user_reviews %}
                <div class="review">{{ review.content }}</div>
            {% endfor %}
        </div>
    {% endcache %}
    
    <!-- 言語別の価格表示 -->
    {% get_current_language as LANGUAGE_CODE %}
    {% cache 3600 product_price product.id LANGUAGE_CODE %}
        <div class="price">
            {% if LANGUAGE_CODE == 'ja' %}
                ¥{{ product.price_jpy|floatformat:0 }}
            {% else %}
                ${{ product.price_usd|floatformat:2 }}
            {% endif %}
        </div>
    {% endcache %}
</div>

キャッシュ無効化とクリア

from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    
    def update(self, request, *args, **kwargs):
        response = super().update(request, *args, **kwargs)
        
        # 更新時にキャッシュを無効化
        product = self.get_object()
        
        # ビューレベルキャッシュの無効化
        cache_key = f'product_detail_{product.id}'
        cache.delete(cache_key)
        
        # テンプレートフラグメントキャッシュの無効化
        fragment_key = make_template_fragment_key(
            'product_description', 
            [product.id]
        )
        cache.delete(fragment_key)
        
        # パターンマッチングによる関連キャッシュの削除
        pattern_keys = cache.keys(f'product_{product.id}_*')
        if pattern_keys:
            cache.delete_many(pattern_keys)
        
        return response
    
    def create(self, request, *args, **kwargs):
        response = super().create(request, *args, **kwargs)
        
        # 商品リストのキャッシュをクリア
        cache.delete('product_list_cache')
        
        return response

# カスタム管理コマンドでキャッシュクリア
# management/commands/clear_cache.py
from django.core.management.base import BaseCommand
from django.core.cache import cache

class Command(BaseCommand):
    help = 'Clear all cache'
    
    def add_arguments(self, parser):
        parser.add_argument(
            '--pattern',
            type=str,
            help='Clear cache keys matching pattern'
        )
    
    def handle(self, *args, **options):
        if options['pattern']:
            keys = cache.keys(options['pattern'])
            cache.delete_many(keys)
            self.stdout.write(f'Deleted {len(keys)} cache keys')
        else:
            cache.clear()
            self.stdout.write('All cache cleared')

非同期キャッシュ操作

from django.core.cache import cache
from asgiref.sync import sync_to_async

# 非同期ビューでのキャッシュ使用
async def async_product_detail(request, product_id):
    cache_key = f'product_{product_id}'
    
    # 非同期でキャッシュから取得
    product = await cache.aget(cache_key)
    
    if product is None:
        # データベースから非同期取得
        product = await Product.objects.aget(id=product_id)
        # 非同期でキャッシュに保存
        await cache.aset(cache_key, product, 3600)
    
    return JsonResponse({
        'id': product.id,
        'name': product.name,
        'price': product.price
    })

# 非同期での複数キー操作
async def async_multiple_products(request):
    product_ids = request.GET.getlist('ids')
    cache_keys = [f'product_{pid}' for pid in product_ids]
    
    # 複数キーを非同期で取得
    cached_products = await cache.aget_many(cache_keys)
    
    return JsonResponse({
        'products': list(cached_products.values())
    })

# カスタム非同期キャッシュヘルパー
class AsyncCacheHelper:
    @staticmethod
    async def get_or_set_async(key, async_callable, timeout=3600):
        """非同期版のget_or_set"""
        value = await cache.aget(key)
        if value is None:
            value = await async_callable()
            await cache.aset(key, value, timeout)
        return value
    
    @staticmethod
    async def invalidate_pattern_async(pattern):
        """パターンマッチングによる非同期削除"""
        # 同期メソッドを非同期化
        keys = await sync_to_async(cache.keys)(pattern)
        if keys:
            await cache.adelete_many(keys)
        return len(keys)

キャッシュバージョニングとキー管理

from django.core.cache import cache
from django.conf import settings

class VersionedCache:
    def __init__(self, version=None):
        self.version = version or getattr(settings, 'CACHE_VERSION', 1)
    
    def make_key(self, key, version=None):
        version = version or self.version
        return f'v{version}:{key}'
    
    def get(self, key, default=None, version=None):
        versioned_key = self.make_key(key, version)
        return cache.get(versioned_key, default)
    
    def set(self, key, value, timeout=None, version=None):
        versioned_key = self.make_key(key, version)
        return cache.set(versioned_key, value, timeout)
    
    def delete(self, key, version=None):
        versioned_key = self.make_key(key, version)
        return cache.delete(versioned_key)
    
    def incr_version(self, key):
        """キーのバージョンを増分"""
        return cache.incr_version(key)

# 使用例
vcache = VersionedCache(version=2)
vcache.set('user_data', user_info, 3600)
user_data = vcache.get('user_data')

# バージョンアップによる全キャッシュ無効化
cache.incr_version('user_data')  # バージョン3になり、過去のキャッシュは無効化