Unit of Work

UnitOfWork wraps one database transaction. Open it as an async with context manager: the transaction commits automatically on clean exit and rolls back if an exception propagates out.

from alchemiq import UnitOfWork, Repository
from myapp.models import Order, Payment

orders   = Repository(Order)
payments = Repository(Payment)

async with UnitOfWork() as uow:
    order   = await orders.create(total=99_00, status="pending")
    payment = await payments.create(order_id=order.id, amount=99_00)
# both rows committed atomically here

Commit and rollback

By default the transaction is managed automatically:

  • Clean exit - the block finishes without raising -> commit.

  • Exception - an exception propagates out of the block -> rollback, then the exception is re-raised.

You can also commit or roll back manually from inside the block:

async with UnitOfWork() as uow:
    order = await orders.create(total=50_00, status="pending")
    await uow.commit()      # flush + commit mid-block
    # subsequent operations start a new transaction in the same session
async with UnitOfWork() as uow:
    await orders.create(total=50_00, status="pending")
    await uow.rollback()    # abort and clear pending changes

Note

commit() and rollback() may only be called on the outermost UnitOfWork. Calling them on a nested (joined) instance raises RuntimeError - see the Nested UnitOfWork section below.


Repositories within a UnitOfWork

Any Repository or QuerySet call made inside an active UnitOfWork block automatically uses the same underlying session. This ensures all operations in the block are part of the same atomic transaction without any extra wiring:

async with UnitOfWork() as uow:
    user  = await Repository(User).create(name="Alice", email="alice@example.com")
    audit = await Repository(AuditLog).create(action="user.created", target_id=user.id)
    # user + audit_log row committed together

Nested UnitOfWork - reentrancy

UnitOfWork is reentrant. A nested async with UnitOfWork() block joins the already-active session - its __aexit__ is a no-op. Only the outermost block commits and closes the session:

async with UnitOfWork() as outer:
    order = await orders.create(total=100_00, status="new")

    async with UnitOfWork() as inner:
        # inner.session is outer.session - same transaction
        await payments.create(order_id=order.id, amount=100_00)
        # inner exits: no commit, no close

# outer exits: single commit covers both rows

This makes it safe to call service functions that open their own UnitOfWork from within an outer transaction - the inner one silently joins rather than starting a competing transaction.


Savepoints

For partial rollback within a single transaction use uow.savepoint(). An exception inside the savepoint block rolls back only to that savepoint, leaving changes made before it intact:

async with UnitOfWork() as uow:
    good = await orders.create(total=50_00, status="confirmed")
    await uow.session.flush()         # make id available

    try:
        async with uow.savepoint():
            bad = await orders.create(total=-1, status="invalid")
            raise ValueError("bad order")   # rolls back to savepoint only
    except ValueError:
        pass  # good order is still pending

# only the good order is committed

FastAPI integration

In FastAPI applications the UnitOfWork can be injected as a dependency so you never open or close it manually in route handlers. See the FastAPI integration guide for the dependency setup.