tg-caching

Django caching patterns for the World of Darkness application. Use when implementing view caching, queryset caching, cache invalidation, or optimizing database queries. Triggers on performance optimization, cache configuration, slow query fixes, or Redis cache usage.

$ Installieren

git clone https://github.com/charlesmsiegel/tg /tmp/tg && cp -r /tmp/tg/.claude/skills/tg-caching ~/.claude/skills/tg

// tip: Run this command in your terminal to install the skill


name: tg-caching description: Django caching patterns for the World of Darkness application. Use when implementing view caching, queryset caching, cache invalidation, or optimizing database queries. Triggers on performance optimization, cache configuration, slow query fixes, or Redis cache usage.

Caching Patterns

Configuration

Development (LocMemCache)

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

Production (Redis)

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": os.environ.get("REDIS_URL"),
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "CONNECTION_POOL_KWARGS": {"max_connections": 50},
            "COMPRESSOR": "django_redis.compressors.zlib.ZlibCompressor",
        },
    }
}

Cache Timeout Constants

Use in core/cache.py:

CACHE_TIMEOUT_SHORT = 60        # 1 minute
CACHE_TIMEOUT_MEDIUM = 300      # 5 minutes
CACHE_TIMEOUT_LONG = 900        # 15 minutes
CACHE_TIMEOUT_VERY_LONG = 3600  # 1 hour
CACHE_TIMEOUT_DAY = 86400       # 24 hours

Caching Strategies

1. View-Level Caching

For static pages identical for all users:

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

@method_decorator(cache_page(60 * 15), name='dispatch')
class MeritFlawListView(ListView):
    model = MeritFlaw
    template_name = "characters/core/meritflaw/list.html"

Use for: Reference data lists, public pages, rarely-changing content. Don't use for: User-specific pages, permission-gated content.

2. Queryset Caching

For expensive database queries:

from core.cache import cache_function, CACHE_TIMEOUT_MEDIUM

class CharacterDetailView(DetailView):
    @staticmethod
    @cache_function(timeout=CACHE_TIMEOUT_MEDIUM, key_prefix="char_scenes")
    def get_character_scenes(character_id):
        return list(
            Scene.objects.filter(characters__id=character_id)
            .select_related("chronicle", "location")
            .prefetch_related("characters")
            .order_by("-date_of_scene")
        )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["scenes"] = self.get_character_scenes(self.object.id)
        return context

3. Manual Cache Access

from django.core.cache import cache

# Set with timeout
cache.set('my_key', data, timeout=300)

# Get with default
data = cache.get('my_key', default=None)

# Delete
cache.delete('my_key')

# Get or set pattern
def get_expensive_data():
    key = 'expensive_calculation'
    result = cache.get(key)
    if result is None:
        result = perform_expensive_calculation()
        cache.set(key, result, timeout=300)
    return result

4. Template Fragment Caching

{% load cache %}

{% cache 900 character_details character.id %}
    <div class="character-details">
        {{ character.name }}
        {{ character.description }}
    </div>
{% endcache %}

Cache Key Generator

class CacheKeyGenerator:
    PREFIX = "tg"

    @classmethod
    def make_model_key(cls, model_class, **filters):
        filter_str = ":".join(f"{k}={v}" for k, v in sorted(filters.items()))
        return f"{cls.PREFIX}:queryset:{model_class.__name__}:{filter_str}"

    @classmethod
    def make_view_key(cls, view_name, **params):
        param_str = ":".join(f"{k}={v}" for k, v in sorted(params.items()))
        return f"{cls.PREFIX}:view:{view_name}:{param_str}"

Cache Invalidation

On Model Save

from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver

@receiver([post_save, post_delete], sender=Character)
def invalidate_character_cache(sender, instance, **kwargs):
    cache.delete(f"tg:queryset:Character:id={instance.id}")
    cache.delete(f"tg:queryset:Character:chronicle={instance.chronicle_id}")

Manual Invalidation

# Single key
cache.delete('specific_key')

# Pattern-based (Redis only)
from django_redis import get_redis_connection
con = get_redis_connection("default")
keys = con.keys("tg:queryset:Character:*")
if keys:
    con.delete(*keys)

# Clear all (use sparingly!)
cache.clear()

Best Practices

Do Cache

  • Reference data (merits/flaws, archetypes, clans)
  • Expensive aggregations
  • API responses
  • Static content lists

Don't Cache

  • User-specific permissions
  • Frequently changing data
  • Data displayed immediately after modification
  • Session-dependent content

Cache Duration Guidelines

Content TypeTimeoutExample
Static reference15-60 minMerit/flaw lists
Semi-static5-15 minCharacter lists
News/updates1-5 minHome page
Real-timeDon't cacheChat, notifications

Combine with Query Optimization

# GOOD: Cache an optimized query
@cache_function(timeout=300)
def get_characters():
    return Character.objects.select_related('owner').prefetch_related('merits_and_flaws')

# BAD: Caching doesn't fix N+1
@cache_function(timeout=300)
def get_characters():
    return Character.objects.all()  # N+1 on access

Debugging

Check Cache Status

from django.core.cache import cache

# Test connection
cache.set('test', 'value')
assert cache.get('test') == 'value'
print("Cache working!")

# Check Redis keys (Redis only)
from django_redis import get_redis_connection
con = get_redis_connection("default")
print(f"Total keys: {len(con.keys('*'))}")

Clear Cache

# Django shell
python manage.py shell
>>> from django.core.cache import cache
>>> cache.clear()

# Redis CLI
redis-cli FLUSHDB