django-cacheops
django-cacheops is a slick ORM cache for Django, offering automatic granular event-driven invalidation. It leverages Redis for high-performance caching. Currently at version 7.2, it maintains an active development cycle with regular updates supporting recent Django and Python versions.
Warnings
- breaking In version 7.0, `CACHEOPS_ENABLED` default changed from `False` to `True`, and `CACHEOPS_TIMEOUT` changed from `None` (never expire) to `3600` seconds (1 hour). This can lead to unexpected caching behavior or premature cache expiration if your project relied on the previous defaults without explicit configuration.
- breaking As of version 7.0, django-cacheops requires Python 3.7+ and Django 3.2+.
- gotcha Caching within database transactions can lead to stale reads if the transaction is rolled back or if other processes read cached data before the transaction commits. Cacheops might cache pre-commit data.
- gotcha Incorrect Redis configuration (`CACHEOPS_REDIS`) or an inaccessible Redis server will cause `ConnectionError`s or make Cacheops silently fail (act as a no-op, passing queries through to the DB).
Install
-
pip install django-cacheops
Imports
- cached_queryset
from cacheops import cached_queryset
- invalidate_model
from cacheops import invalidate_model
- invalidate_all
from cacheops import invalidate_all
Quickstart
import os
import django
from django.conf import settings
from django.db import models
# Minimal Django settings for a runnable example
# In a real project, these would be in your settings.py
if not settings.configured:
settings.configure(
INSTALLED_APPS=[
'django.contrib.auth',
'django.contrib.contenttypes',
'cacheops', # Add cacheops to your installed apps
'myapp', # A dummy app for model definition
],
CACHEOPS_REDIS=os.environ.get('CACHEOPS_REDIS', 'redis://localhost:6379/1'),
# CACHEOPS_ENABLED defaults to True since v7.0
# CACHEOPS_TIMEOUT defaults to 3600 (1 hour) since v7.0
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
},
USE_TZ=True,
TIME_ZONE='UTC',
)
django.setup()
# Define a simple Django model. For demonstration, we use 'myapp' as app_label.
class Post(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'myapp' # Required when defining models outside an actual app directory
def __str__(self):
return self.title
# Ensure the database schema is created for the model (for in-memory SQLite)
from django.db import connection
with connection.schema_editor() as schema_editor:
schema_editor.create_model(Post)
print(f"Using Redis connection string: {settings.CACHEOPS_REDIS}")
# Create some sample data
Post.objects.create(title="First Post", body="Content of the first post.")
Post.objects.create(title="Second Article", body="More content here.")
# --- Example 1: Caching a Queryset ---
print("\n--- Example 1: Caching a Queryset ---")
# The .cache() method makes the queryset result be cached.
# First call will hit the database and store the result in cache.
posts = Post.objects.filter(title__icontains='post').cache().all()
print(f"Posts (first fetch, hits DB and caches): {[p.title for p in posts]}")
# Subsequent calls with the same query parameters will hit the cache.
posts_cached = Post.objects.filter(title__icontains='post').cache().all()
print(f"Posts (second fetch, should hit cache): {[p.title for p in posts_cached]}")
# --- Example 2: Invalidating Cache ---
from cacheops import invalidate_model
print("\n--- Example 2: Invalidating Cache ---")
# Update an object (usually auto-invalidates if configured)
post_to_update = Post.objects.get(title="First Post")
post_to_update.title = "Updated First Post"
post_to_update.save()
# Explicitly invalidate all caches related to the Post model
invalidate_model(Post)
print("Post model cache explicitly invalidated.")
# Fetching again will hit the DB to get the updated data and re-cache.
updated_posts = Post.objects.filter(title__icontains='post').cache().all()
print(f"Posts after update and invalidation: {[p.title for p in updated_posts]}")
# --- Example 3: Using @cached_queryset decorator ---
from cacheops import cached_queryset
print("\n--- Example 3: Using @cached_queryset decorator ---")
@cached_queryset
def get_latest_articles(limit=1):
print(" (Fetching from DB inside get_latest_articles)")
return Post.objects.order_by('-created_at')[:limit]
# First call of the decorated function
latest1 = get_latest_articles()
print(f"Latest article (first call): {[p.title for p in latest1]}")
# Second call of the decorated function should hit the cache
latest2 = get_latest_articles()
print(f"Latest article (second call, should hit cache): {[p.title for p in latest2]}")