Skip to content

Use Case 8: Document Population

Scenario:
An API needs to return a single, denormalised JSON payload that includes related documents (e.g., a User with an embedded Profile, or an Order with its LineItems) without the caller having to issue multiple requests.


📦 What’s New?

Component Description
PopulateRule Declarative rule that tells the engine how to populate a field.
Key attributes:
field_name – Target attribute on the source model.
collection_name – Collection that stores the referenced documents.
ref_field – Name of the reference field (normally an ObjectId).
• Optional nested_rules for deep population, max_depth to bound recursion, and filter / projection for fine‑grained queries.
PopulationEngine Core engine that resolves a list of PopulateRules recursively. It:
• Detects circular references and raises CircularReferenceError.
• Honors per‑rule max_depth and a global global_max_depth (default 10).
• Supports MongoDB filters and projections for each populated relation.
PopulatingRepository Extends BaseRepository with automatic population support. Stores a reference to a PopulationEngine and a list of PopulateRules, exposing two helpers:
_populate(document) – Returns the same document with the requested relations populated.
_depopulate(document) – Strips populated fields (useful before serialisation).
Cache‑aware population (optional) – The engine works with any CacheBackend. When used together with CachedBaseRepository, populated documents benefit from caching in the same way as regular CRUD results.

🚀 Quick Example

Python
from bson import ObjectId
from mongo_ops import BaseDocument, PopulatingRepository, ModelRegistry
from mongo_ops.populate import PopulateRule, PopulationEngine
from mongo_ops.cache import InMemoryCacheBackend

# ----------------------------------------------------------------------
# 1️⃣ Define the data 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 (filled by engine)

# ----------------------------------------------------------------------
# 2️⃣ (Optional) Set up a cache; helpful for deep graphs
# ----------------------------------------------------------------------
cache = InMemoryCacheBackend(max_entries=10_000, default_ttl=300)
ModelRegistry.set_cache_backend(cache)

# ----------------------------------------------------------------------
# 3️⃣ Create a PopulationEngine – repositories will be registered later
# ----------------------------------------------------------------------
engine = PopulationEngine({})

# ----------------------------------------------------------------------
# 4️⃣ Declare how the `profile` field should be populated
# ----------------------------------------------------------------------
profile_rule = PopulateRule(
    field_name="profile",
    collection_name="profiles",
    ref_field="profile_id",
    # No further nesting in this simple example
)

# ----------------------------------------------------------------------
# 5️⃣ Repository that uses the engine + rule
# ----------------------------------------------------------------------
class UserRepository(PopulatingRepository[User]):
    def __init__(self):
        super().__init__(
            collection_name="users",
            model=User,
            population_engine=engine,
            populate_rules=[profile_rule],
        )

# ----------------------------------------------------------------------
# 6️⃣ Register the repository with the engine so it can resolve refs
# ----------------------------------------------------------------------
engine.register_repo("profiles", PopulatingRepository[Profile]("profiles", Profile))
engine.register_repo("users", UserRepository())

# ----------------------------------------------------------------------
# 7️⃣ FastAPI lifespan – initialise DB, indexes, and the cache
# ----------------------------------------------------------------------
from mongo_ops import MongoConnectionManager
from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app):
    async with MongoConnectionManager.lifespan(
        uri="mongodb://localhost:27017",
        db_name="mydb"
    ):
        await ModelRegistry.initialize_all()
        # If you set up a cache backend (see step 2), initialize it here
        await ModelRegistry.initialize_cache()
        yield