Skip to content

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_depth conservatively; a value of 2‑3 is enough for most graphs.
  • When you know there are no cycles, you can omit max_depth on nested rules.
  • The CircularReferenceError includes the visited path, which helps debugging.