Skip to content

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_id to keep the caching logic simple; you could also create a mixin that composes CachedBaseRepository and PopulatingRepository.
  • 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’s config.key_prefix, the same prefix will be used for populated results.