Use Case 9: Nested Document Population & Circular‑Ref Handling
Scenario:
A service must return an Author object with its books populated, and each book must include its publisher. The data model contains a possible circular reference (Author.mentor_id → another Author).
📦 What’s New?
| Component | Description |
|---|---|
PopulateRule (nested) |
Allows you to declare a hierarchy of rules. In this example we populate books on Author, then publisher on each Book. |
max_depth |
Prevents infinite recursion when circular references exist. |
| Circular‑reference guard | PopulationEngine raises CircularReferenceError if a cycle exceeds max_depth or is revisited. |
🚀 Example
Python
from bson import ObjectId
from mongo_ops import BaseDocument, PopulatingRepository, ModelRegistry
from mongo_ops.populate import PopulateRule, PopulationEngine
# ----------------------------------------------------------------------
# 1️⃣ Define the data models
# ----------------------------------------------------------------------
class Publisher(BaseDocument):
name: str = ""
country: str = ""
class Book(BaseDocument):
title: str = ""
publisher_id: ObjectId = None # Reference to Publisher
publisher: Publisher | None = None
class Author(BaseDocument):
name: str = ""
book_ids: list[ObjectId] = [] # References to Book documents
books: list[Book] | None = None
mentor_id: ObjectId = None # Optional circular reference to another Author
mentor: "Author" | None = None
# ----------------------------------------------------------------------
# 2️⃣ Create a PopulationEngine
# ----------------------------------------------------------------------
engine = PopulationEngine({})
# ----------------------------------------------------------------------
# 3️⃣ Declare nested rules
# ----------------------------------------------------------------------
# Populate the Publisher inside each Book
book_rule = PopulateRule(
field_name="publisher",
collection_name="publishers",
ref_field="publisher_id",
)
# Populate books on Author, nesting the book_rule
author_rule = PopulateRule(
field_name="books",
collection_name="books",
ref_field="book_ids",
nested_rules=[book_rule],
max_depth=3, # safety net for deep graphs
)
# Optional mentor rule (demonstrates circular‑ref handling)
mentor_rule = PopulateRule(
field_name="mentor",
collection_name="authors",
ref_field="mentor_id",
max_depth=2,
)
# ----------------------------------------------------------------------
# 4️⃣ Repository that uses the engine + rules
# ----------------------------------------------------------------------
class AuthorRepository(PopulatingRepository[Author]):
def __init__(self):
super().__init__(
collection_name="authors",
model=Author,
population_engine=engine,
populate_rules=[author_rule, mentor_rule],
)
# ----------------------------------------------------------------------
# 5️⃣ Register all repositories with the engine
# ----------------------------------------------------------------------
engine.register_repo("publishers", PopulatingRepository[Publisher]("publishers", Publisher))
engine.register_repo("books", PopulatingRepository[Book]("books", Book))
engine.register_repo("authors", AuthorRepository())
# ----------------------------------------------------------------------
# 6️⃣ FastAPI lifespan – initialise DB & cache (if any)
# ----------------------------------------------------------------------
from mongo_ops import MongoConnectionManager
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
async with MongoConnectionManager.lifespan(
uri="mongodb://localhost:27017",
db_name="library",
):
await ModelRegistry.initialize_all()
await ModelRegistry.initialize_cache()
yield
💡 Tips
- Use
max_depthconservatively; a value of2‑3is enough for most graphs. - When you know there are no cycles, you can omit
max_depthon nested rules. - The
CircularReferenceErrorincludes the visited path, which helps debugging.