Use Case 10: Caching + Population (Read‑Through + Populated Docs)
Scenario:
A service needs fast reads of a User document and its related Profile. The repository should cache the final populated result so subsequent calls hit the cache directly.
📦 What’s New?
| Component | Description |
|---|---|
CachedBaseRepository |
Provides transparent ID‑based caching for CRUD operations. |
PopulatingRepository |
Adds automatic population of referenced documents. |
| Combined usage | By inheriting from CachedBaseRepository and wiring a PopulationEngine, the repository caches the populated document, eliminating both DB and population overhead on cache hits. |
🚀 Example
Python
from bson import ObjectId
from mongo_ops import BaseDocument, CachedBaseRepository, ModelRegistry
from mongo_ops.populate import PopulateRule, PopulationEngine
from mongo_ops.cache import InMemoryCacheBackend
# ----------------------------------------------------------------------
# 1️⃣ Define models
# ----------------------------------------------------------------------
class Profile(BaseDocument):
avatar_url: str = ""
bio: str = ""
class User(BaseDocument):
username: str = ""
email: str = ""
profile_id: ObjectId = None # Reference to Profile
profile: Profile | None = None # Populated field
# ----------------------------------------------------------------------
# 2️⃣ Initialise cache backend (in‑memory example)
# ----------------------------------------------------------------------
cache = InMemoryCacheBackend(max_entries=20_000, default_ttl=600)
ModelRegistry.set_cache_backend(cache)
# ----------------------------------------------------------------------
# 3️⃣ Set up a PopulationEngine (repositories will be registered later)
# ----------------------------------------------------------------------
engine = PopulationEngine({})
profile_rule = PopulateRule(
field_name="profile",
collection_name="profiles",
ref_field="profile_id",
)
# ----------------------------------------------------------------------
# 4️⃣ Cached + Populating repository
# ----------------------------------------------------------------------
class UserRepository(CachedBaseRepository[User]):
def __init__(self):
super().__init__(
collection_name="users",
model=User,
cache_backend=cache,
config=None, # defaults to enabled=True, backend="memory"
)
# Attach the population engine after the base repo is ready
self.population_engine = engine
self._populate_rules = [profile_rule]
# Override get_by_id to include population before caching
async def get_by_id(self, id):
# First attempt cache lookup (as in CachedBaseRepository)
cached = await super().get_by_id(id)
if cached:
return cached
# Not in cache – fetch from DB and populate
doc = await super().get_by_id(id) # this will hit the DB (no cache hit)
if doc is None:
return None
# Populate related docs
populated = await engine.populate(doc, self._populate_rules)
# Store the fully populated result in cache for next call
await self._cache.set(self._cache_key(id), populated)
return populated
# ----------------------------------------------------------------------
# 5️⃣ Register repositories with the engine for population resolution
# ----------------------------------------------------------------------
engine.register_repo("profiles", PopulatingRepository[Profile]("profiles", Profile))
engine.register_repo("users", UserRepository())
# ----------------------------------------------------------------------
# 6️⃣ FastAPI lifespan – initialise DB, create indexes, and start cache cleanup
# ----------------------------------------------------------------------
from mongo_ops import MongoConnectionManager
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
async with MongoConnectionManager.lifespan(
uri="mongodb://localhost:27017",
db_name="app_db",
):
await ModelRegistry.initialize_all()
await ModelRegistry.initialize_cache()
yield
💡 Tips
- The example overrides
get_by_idto keep the caching logic simple; you could also create a mixin that composesCachedBaseRepositoryandPopulatingRepository. - Remember to call
await ModelRegistry.initialize_cache()after the DB connection is ready; otherwise the background cleanup task will never start. - Cache keys are generated by
CachedBaseRepository._cache_key(id). If you change the repository’sconfig.key_prefix, the same prefix will be used for populated results.