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=sessionto thefind_one/findcalls. - Testing: In unit tests you can mock
TransactionManager.start_sessionto ensure the helpers are called.