142 lines
4.0 KiB
Markdown
142 lines
4.0 KiB
Markdown
# Async SQLAlchemy Session Management
|
|
|
|
!!! info "Primary sources"
|
|
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
|
- [SQLAlchemy session basics](https://docs.sqlalchemy.org/en/21/orm/session_basics.html)
|
|
- [FastAPI dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/)
|
|
|
|
??? abstract "Decision metadata"
|
|
- Status: adopted
|
|
- Decision level: mandatory
|
|
- Applies to: api-runtime, workers, tests
|
|
- Last reviewed: 2026-06-17
|
|
|
|
---
|
|
|
|
## Purpose
|
|
|
|
Define one canonical session model for FastAPI + SQLAlchemy asyncio:
|
|
|
|
- configure one shared session factory,
|
|
- create one AsyncSession per request or per unit-of-work,
|
|
- never share one AsyncSession across concurrent tasks.
|
|
|
|
---
|
|
|
|
## Scope and Non-Goals
|
|
|
|
- In scope: session factory creation, FastAPI dependency wiring, request/task scoping, transaction demarcation.
|
|
- Out of scope: ORM model design, query optimization strategy, schema migration tooling.
|
|
|
|
---
|
|
|
|
## Rules
|
|
|
|
- Create `async_sessionmaker` once from app-owned AsyncEngine.
|
|
- Use a fresh AsyncSession for each request or explicit unit-of-work.
|
|
- 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.
|
|
|
|
---
|
|
|
|
## Canonical FastAPI Dependency Pattern
|
|
|
|
```python
|
|
from collections.abc import AsyncIterator
|
|
|
|
from fastapi import Depends, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
|
|
def get_session_factory(request: Request) -> async_sessionmaker[AsyncSession]:
|
|
return request.app.state.session_factory
|
|
|
|
|
|
async def get_db_session(
|
|
session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory),
|
|
) -> AsyncIterator[AsyncSession]:
|
|
async with session_factory() as session:
|
|
yield session
|
|
```
|
|
|
|
Route usage:
|
|
|
|
```python
|
|
from fastapi import APIRouter, Depends
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/items")
|
|
async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
|
|
async with session.begin():
|
|
# write operations here
|
|
...
|
|
return {"status": "ok"}
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration Guidance
|
|
|
|
Typical session factory setup:
|
|
|
|
```python
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
session_factory = async_sessionmaker(
|
|
engine,
|
|
class_=AsyncSession,
|
|
expire_on_commit=False,
|
|
)
|
|
```
|
|
|
|
Notes:
|
|
|
|
- `expire_on_commit=False` is commonly preferred in asyncio applications to reduce accidental post-commit reload behavior.
|
|
- `AsyncSession.refresh()` is preferred over broad expiration patterns when state refresh is needed.
|
|
|
|
---
|
|
|
|
## Concurrency Rules
|
|
|
|
- One session per concurrent task.
|
|
- If work fans out into parallel tasks, each task receives its own AsyncSession.
|
|
- Pass sessions explicitly to service functions; avoid mutable global session state.
|
|
|
|
---
|
|
|
|
## Anti-Patterns
|
|
|
|
- A singleton/global AsyncSession reused across requests.
|
|
- Sharing one AsyncSession across parallel tasks.
|
|
- Hidden session creation in lower repository helpers with no caller control.
|
|
- Mixing commit/rollback ownership across layers without a declared boundary.
|
|
|
|
---
|
|
|
|
## Operational Checks
|
|
|
|
- Exactly one `async_sessionmaker` is registered in app lifecycle.
|
|
- Request handlers receive sessions from one canonical dependency.
|
|
- No code path creates AsyncSession in module import side effects.
|
|
- Background jobs and API handlers each create task-local sessions.
|
|
|
|
---
|
|
|
|
## Testing Checks
|
|
|
|
- Dependency override exists for test session factory.
|
|
- Rollback behavior is verified for failed write units.
|
|
- Parallel-task tests verify no shared AsyncSession instances.
|
|
- Lifespan tests confirm session factory is initialized and teardown-safe.
|
|
|
|
---
|
|
|
|
## Migration Notes
|
|
|
|
- If current code uses global/shared sessions, fix scope first before refactoring query style.
|
|
- If legacy sync patterns are present, keep session boundary rules stable while migrating incrementally.
|