started crud reference

This commit is contained in:
John Lancaster
2026-07-26 19:35:01 -05:00
parent d999a04144
commit aed2e41ef0
5 changed files with 416 additions and 4 deletions
@@ -36,6 +36,7 @@ Define one canonical session model for FastAPI + SQLAlchemy asyncio:
- Do not share AsyncSession across `asyncio.gather()` or parallel tasks.
- Prefer direct dependency injection over global scoped-session patterns in new code.
- Use explicit transaction boundaries (`async with session.begin():`) for writes.
- When a use case accepts an optional session, borrow only an active caller-owned transaction or own the complete session-and-transaction scope.
---
@@ -132,6 +133,54 @@ Do not turn this into an implicit unit-of-work helper that sometimes commits. Wh
---
## Optional Transaction Ownership
Use a separate context manager when a service or use-case function must support both a caller-owned transaction and a standalone transaction. A supplied session must already be inside a transaction; otherwise the helper creates a session and transaction together with `async_sessionmaker.begin()`:
```python
@asynccontextmanager
async def transaction_scope(
*,
database_url: str,
session: AsyncSession | None = None,
session_factory: async_sessionmaker[AsyncSession] | None = None,
) -> AsyncIterator[AsyncSession]:
if session is not None:
if not session.in_transaction():
raise RuntimeError("A supplied session must have an active transaction")
yield session
return
active_factory = session_factory or get_session_factory(database_url)
async with active_factory.begin() as owned_session:
yield owned_session
```
This helper makes transaction ownership follow the same explicit borrow-or-own mechanics as session ownership:
- A supplied session and its active transaction remain caller-owned. The helper does not commit, roll back, or close them.
- Without a supplied session, the helper owns the session and transaction. Successful exit commits; exceptional exit rolls back; either path closes the session.
- A supplied factory overrides cached resolution only when the helper must create a session.
- Use this helper only at a complete operation, service, or use-case boundary. A public CRUD function or repository method may be such a boundary when its optional-session contract explicitly states that omitting the session owns and commits one transaction. Never use it inside a lower-level session-required helper.
- Do not silently begin a transaction on a supplied session. That would make commit ownership depend on hidden helper behavior.
Callers that supply a session make their ownership visible with an outer transaction:
```python
async with session_factory() as session:
async with session.begin():
await run_use_case(..., session=session)
```
Standalone callers omit the session and let the use case own the complete unit of work:
```python
await run_use_case(...)
```
---
## Repository and Function Boundaries
Pass the database URL to repository constructors. The repository stores repeatable database configuration, not mutable session state, and `session_scope()` resolves the cached factory when a standalone operation needs a session. An optional factory override keeps tests independent:
@@ -270,6 +319,7 @@ async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
- Hidden session creation in lower access functions with no caller control.
- Closing or committing a session supplied by the caller.
- Starting a new transaction inside a helper that may receive a session already in a transaction.
- Silently starting or committing a transaction on a supplied session.
- Mixing commit/rollback ownership across layers without a declared boundary.
---
@@ -290,6 +340,8 @@ async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
- Session-taking access functions accept a transaction-scoped test session directly.
- Optional-session tests verify that borrowed sessions remain open and created sessions close.
- Optional-session tests verify that neither path commits implicitly.
- Optional-transaction tests verify supplied sessions require an active transaction and remain caller-owned.
- Optional-transaction tests verify owned transactions commit on success, roll back on failure, and close their sessions.
- Cache tests clear `get_session_factory` before and after replacing engines.
- Dependency override exists for the FastAPI session factory.
- Rollback behavior is verified for failed write units.