Caching¶
alchemiq provides an optional repository-level cache layer. When enabled, reads are served from cache and invalidated automatically on write. The cache is fail-open: a backend error is logged but never propagates to the caller.
Installation¶
The in-process backend (InMemoryCache) is part of the core
package and requires no extra dependencies. For Redis:
pip install "alchemiq[redis,postgres]"
Configuring a backend¶
Call configure_cache() once at startup, before any repository
uses the cache:
from alchemiq.cache import InMemoryCache, configure_cache
# zero-dependency in-process cache (good for testing and single-process services)
configure_cache(backend=InMemoryCache(default_ttl=120))
# Redis (requires [redis] extra)
configure_cache(url="redis://localhost:6379/0")
# Redis with custom TTL and key namespace
configure_cache(url="redis://localhost:6379/0", default_ttl=300, namespace="myapp")
reset_cache() clears the global backend - useful in test
teardown:
from alchemiq.cache import reset_cache
reset_cache()
The CacheBackend protocol¶
CacheBackend is a structural (duck-typed) protocol. Any
object that exposes the five async methods below satisfies it:
Method |
Description |
|---|---|
|
Return cached |
|
Store |
|
Delete one or more keys; no-ops on missing keys |
|
Atomically increment an integer counter, return new value |
|
Delete all keys matching a glob pattern |
Plus two required attributes: default_ttl: int and namespace: str.
InMemoryCache implements this protocol in pure Python (no extra
dependencies). RedisCache is built automatically by configure_cache
when a Redis url is supplied.
Enabling cache on a repository¶
Set cache = True on a Repository subclass, or pass it at construction time:
from alchemiq import Repository
from alchemiq.cache import configure_cache, InMemoryCache
configure_cache(backend=InMemoryCache(default_ttl=60))
# subclass - attach cache as a class attribute
class UserRepository(Repository[User]):
cache = True
cache_ttl = 300 # override default TTL for this model
repo = UserRepository()
# ad-hoc - enable for one instance
repo = Repository(User, cache=True, cache_ttl=120)
When cache = True the repository resolves the backend registered by
configure_cache. You may also pass a CacheBackend instance directly
(useful in tests):
from alchemiq.cache import InMemoryCache
repo = Repository(User, cache=InMemoryCache(default_ttl=60))
What is cached¶
Operation |
Cached |
|---|---|
|
Yes - per-object key |
|
Yes - version-keyed query key |
|
Yes |
|
Yes |
|
No - invalidates cache |
The cache uses a version counter per table. Any write bumps the counter, which invalidates all version-keyed query results without a costly scan.
Manual invalidation¶
# invalidate one object by primary key
await repo.cache_evict(pk=42)
# invalidate all cached results for this model
await repo.cache_clear()
cache_evict drops the per-object key and bumps the table version counter.
cache_clear bumps the version counter and scans-deletes all per-object keys
for the model.