Skip to content

Use Case 12: Using TransactionManager.execute_transaction

Scenario: You need to perform several writes across different collections atomically (e.g., creating an Order and updating the Inventory for each ordered item). The low‑level start_session context manager works, but the high‑level execute_transaction helper makes the code cleaner and returns all operation results in a list.


📦 What’s New?

Component Description
TransactionManager.execute_transaction Accepts a list of async callables (each receiving a session) and executes them inside a single MongoDB transaction. Returns a list of each callable’s result.
Automatic rollback If any operation raises an exception, the transaction is aborted and the exception propagates.

🚀 Example

Python
from mongo_ops import MongoConnectionManager, ModelRegistry
from mongo_ops.transactions import TransactionManager
from mongo_ops.repository import BaseRepository
from mongo_ops.models import BaseDocument
from bson import ObjectId

# ----------------------------------------------------------------------
# 1️⃣ Define simple models
# ----------------------------------------------------------------------
class Order(BaseDocument):
    user_id: str
    items: list[dict]  # [{"product_id": ObjectId, "qty": int}]
    total: float = 0.0

class Inventory(BaseDocument):
    product_id: ObjectId
    quantity: int = 0

# ----------------------------------------------------------------------
# 2️⃣ Repositories
# ----------------------------------------------------------------------
class OrderRepo(BaseRepository[Order]):
    def __init__(self):
        super().__init__("orders", Order)

class InventoryRepo(BaseRepository[Inventory]):
    def __init__(self):
        super().__init__("inventory", Inventory)

order_repo = OrderRepo()
inv_repo = InventoryRepo()

# ----------------------------------------------------------------------
# 3️⃣ Transaction helper
# ----------------------------------------------------------------------
async def create_order_with_inventory(order: Order):
    async def insert_order(session):
        # Insert the order document
        doc = order.model_dump(exclude={"id"}, exclude_none=True)
        doc["created_at"] = doc["updated_at"] = __import__("datetime").datetime.utcnow()
        result = await order_repo.collection.insert_one(doc, session=session)
        return await order_repo.collection.find_one({"_id": result.inserted_id}, session=session)

    async def update_inventory(session):
        # Decrease quantity for each ordered item
        for item in order.items:
            await inv_repo.collection.update_one(
                {"product_id": ObjectId(item["product_id"])},
                {"$inc": {"quantity": -item["qty"]}},
                session=session,
            )
        return "inventory-updated"

    # Execute both operations atomically
    results = await TransactionManager.execute_transaction([
        lambda s: insert_order(s),
        lambda s: update_inventory(s),
    ])
    # `results[0]` is the created order document, `results[1]` is the marker string
    return results[0]

# ----------------------------------------------------------------------
# 4️⃣ FastAPI lifespan – initialise DB and run the example endpoint
# ----------------------------------------------------------------------
from fastapi import FastAPI, HTTPException
from contextlib import asynccontextmanager

app = FastAPI()

@asynccontextmanager
async def lifespan(app):
    async with MongoConnectionManager.lifespan(
        uri="mongodb://localhost:27017",
        db_name="shop",
    ):
        await ModelRegistry.initialize_all()
        yield

app.lifespan = lifespan

@app.post("/orders/", response_model=Order)
async def create_order(order: Order):
    created = await create_order_with_inventory(order)
    if created is None:
        raise HTTPException(status_code=400, detail="Transaction failed")
    return created

💡 Tips

  • Return values: Each callable can return whatever you need (inserted document, stats, etc.). The helper collects them in order.
  • Error handling: Raising any exception inside a callable will abort the transaction automatically.
  • Read‑only ops: You can also run queries inside the transaction – just pass session=session to the find_one/find calls.
  • Testing: In unit tests you can mock TransactionManager.start_session to ensure the helpers are called.