Migration Guide¶
From Django's Built-in Cache Backend¶
# Before (Django's Redis backend)
"BACKEND": "django.core.cache.backends.redis.RedisCache"
# After (Valkey)
"BACKEND": "django_cachex.cache.ValkeyCache"
# Or (Redis)
"BACKEND": "django_cachex.cache.RedisCache"
All Django cache options work unchanged. Two behaviors differ on the Valkey and Redis backends:
incr()anddecr()on a missing key start it from 0, like RedisINCRBY, where Django'sRedisCacheraisesValueError. Code that relies on theValueErrorto detect an expired counter must checkhas_key()first. django-cachex'sLocMemCacheandDatabaseCachekeep Django's behavior.clear()deletes only this alias's keys, by pattern overKEY_PREFIXandVERSION, not the whole database.flush_db()runsFLUSHDB.
From django-valkey¶
# Before
"BACKEND": "django_valkey.cache.ValkeyCache"
"OPTIONS": {"CLIENT_CLASS": "django_valkey.client.DefaultClient"}
# After
"BACKEND": "django_cachex.cache.ValkeyCache"
| django-valkey | django-cachex |
|---|---|
CLIENT_CLASS |
Removed. Use the backend class for the topology, such as RedisSentinelCache or ValkeySentinelCache for Sentinel. |
SERIALIZER |
serializer |
COMPRESSOR |
compressor |
CONNECTION_POOL_CLASS |
pool_class |
CONNECTION_POOL_KWARGS |
Flat keys in OPTIONS, forwarded to the pool's from_url() |
PARSER_CLASS |
parser_class |
SENTINELS / SENTINEL_KWARGS |
sentinels / sentinel_kwargs |
get_valkey_connection() |
cache.get_client(write=True) |
cache.lock(key, timeout=30) |
cache.lock(key, lease=30), see Differences |
django_valkey.serializers.json.JSONSerializer |
django_cachex.serializers.json.JsonSerializer |
django_valkey.serializers.msgpack.MSGPackSerializer |
django_cachex.serializers.msgpack.MsgpackSerializer |
django_valkey.compressors.zstd.ZStdCompressor |
django_cachex.compressors.zstd.ZstdCompressor |
Import paths change from django_valkey.* to django_cachex.*, with the class names above.
From django-redis¶
# Before
"BACKEND": "django_redis.cache.RedisCache"
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}
# After
"BACKEND": "django_cachex.cache.RedisCache"
| django-redis | django-cachex |
|---|---|
CLIENT_CLASS |
Removed. Use the backend class for the topology, such as RedisSentinelCache for Sentinel. |
SERIALIZER |
serializer |
COMPRESSOR |
compressor |
CONNECTION_POOL_CLASS |
pool_class |
CONNECTION_POOL_KWARGS |
Flat keys in OPTIONS, forwarded to the pool's from_url() |
PARSER_CLASS |
parser_class |
SENTINELS / SENTINEL_KWARGS |
sentinels / sentinel_kwargs |
get_redis_connection() |
cache.get_client(write=True) |
cache.lock(key, timeout=30) |
cache.lock(key, lease=30), see Differences |
django_redis.serializers.json.JSONSerializer |
django_cachex.serializers.json.JsonSerializer |
django_redis.serializers.msgpack.MSGPackSerializer |
django_cachex.serializers.msgpack.MsgpackSerializer |
django_redis.compressors.zstd.ZStdCompressor |
django_cachex.compressors.zstd.ZstdCompressor |
Import paths change from django_redis.* to django_cachex.*, with the class names above.
Differences from django-redis and django-valkey¶
incr(),decr()andclear()differ in the same way as from Django's built-in backend.ttl()returns-2for a missing key andNonefor a key without expiry.cache.lock()takes the lock's TTL aslease, and its keyword-onlytimeoutcaps how longacquire()waits. An unchangedcache.lock(key, timeout=30)therefore holds the lock without a TTL. A crashed holder then blocks every peer until someone deletes the key by hand.
From django-cachalot¶
The ORM cache is derived from django-cachalot 2.9.1. Replace the app and uninstall cachalot, because running both patches the ORM twice:
Settings¶
The settings move into one CACHEX_ORM dict, without the CACHALOT_ prefix:
# Before
CACHALOT_TIMEOUT = 3600
CACHALOT_UNCACHABLE_TABLES = frozenset(("django_migrations", "django_session"))
# After
CACHEX_ORM = {
"TIMEOUT": 3600,
"UNCACHABLE_TABLES": ("django_session",),
}
| django-cachalot | django-cachex |
|---|---|
CACHALOT_ENABLED, CACHALOT_CACHE, CACHALOT_DATABASES, CACHALOT_ONLY_CACHABLE_TABLES, CACHALOT_ADDITIONAL_TABLES, CACHALOT_FINAL_SQL_CHECK |
The same key without the prefix |
CACHALOT_UNCACHABLE_TABLES |
UNCACHABLE_TABLES. django_migrations is never cached, so it can be left out. |
CACHALOT_TIMEOUT |
TIMEOUT, which defaults to the cache's default timeout, not None |
CACHALOT_ONLY_CACHABLE_APPS, CACHALOT_UNCACHABLE_APPS |
Removed. List the apps' tables, many-to-many tables included, in ONLY_CACHABLE_TABLES or UNCACHABLE_TABLES. |
CACHALOT_CACHE_RANDOM, CACHALOT_CACHE_ITERATORS, CACHALOT_INVALIDATE_RAW |
Removed. Random queries and the results of iterator() are never cached, and raw SQL writes always invalidate. |
CACHALOT_QUERY_KEYGEN, CACHALOT_TABLE_KEYGEN |
QUERY_KEYGEN, TABLE_KEYGEN. They take keyword arguments, so a cachalot keygen needs adapting (see Cache keys). The default query keys already start with their table names, and __in values are sorted before any keygen runs. Keys also go through the cache alias's KEY_FUNCTION. If it tells tenants apart, a write to a table they share invalidates only the writing tenant's results, so list shared tables in UNCACHABLE_TABLES. |
CACHALOT_USE_UNSUPPORTED_DATABASE, CACHALOT_ADDITIONAL_SUPPORTED_DATABASES |
Removed. Only PostgreSQL and SQLite are cached. |
LEASE_TIMEOUT has no cachalot counterpart (see Failures). |
CACHE must name a django-cachex Redis or Valkey backend, or a TrackingCache over one. Its serializer must keep Python types, as the default pickle serializer does. Other backends cache nothing, apart from LocMemCache for tests and single processes (see Caches and databases).
API¶
Import from django_cachex.orm.api instead of cachalot.api:
| django-cachalot | django-cachex |
|---|---|
invalidate() |
invalidate(), with the same arguments |
cachalot_disabled(all_queries=False) |
orm_cache_disabled(), without all_queries, which cachalot 2.9.1 ignored |
get_last_invalidation(*tables_or_models, cache_alias=None, db_alias=None) |
table_generations(*tables_or_models, db_alias="default"), returning generations instead of a timestamp and needing at least one table |
manage.py invalidate_cachalot |
manage.py invalidate_orm_cache, with the same arguments and options |
cachalot.signals.post_invalidation |
Removed. Cache values derived from tables under their table_generations() instead. |
cachalot.* system checks |
cachex_orm.* |
The get_last_invalidation template tag, the Jinja2 extension and the Django Debug Toolbar panel |
Removed |
Cache values under the generations instead of get_last_invalidation(). They are None when a value computed now must not be cached:
# Before
def order_totals():
key = f"totals:{get_last_invalidation(Order, OrderLine)}"
return cache.get_or_set(key, compute_totals, timeout=3600)
# After
def order_totals():
generations = table_generations(Order, OrderLine)
if generations is None:
return compute_totals()
key = "totals:" + ":".join(generations)
return cache.get_or_set(key, compute_totals, timeout=3600)
Behavior differences¶
- Writes need the cache. A write that cannot take its lease raises
InvalidationError, aDatabaseError, unlessENABLEDis off (see Failures). - While a write commits, queries on its tables run against the database and leave no stale result behind (see How invalidation works).
- Subqueries count with their tables wherever they sit, and
Now()anywhere in a query keeps it from being cached. - A MySQL alias in
DATABASESis the errorcachex_orm.E006, and"supported_only"leaves out replicas, the aliases with aTEST["MIRROR"]. - With psycopg2 instead of psycopg 3, queries with JSON, binary or range parameters are not cached.
- A
migratethat applied nothing invalidates nothing. Cachalot invalidated every model after eachmigrate.
Rolling out¶
Cachalot and the ORM cache keep separate keys, so while both run, the writes of one do not invalidate what the other cached. A deployment that stops every process first can switch in one go. A rolling deployment takes three steps:
- Set
CACHALOT_ENABLED = Falseand deploy. - After every process runs with it, replace cachalot with
django_cachex.orm, with"ENABLED": FalseinCACHEX_ORM, and deploy. - After the last cachalot process stops, set
"ENABLED": Trueand deploy.
Cachalot stored its entries without expiry by default. If they live in a cache of their own, clear it. Otherwise they stay until the server evicts them, which it never does under a volatile-* or noeviction policy.