7.1 KiB
FastAPI Database Integration
!!! info "Primary sources"
- FastAPI lifespan events
- FastAPI dependencies with yield
- FastAPI dependency overrides
- SQLAlchemy asyncio extension
Purpose
Connect the framework-independent database tools to FastAPI:
- lifespan enters one application-owned
database_scope(), - application state holds the resulting session factory,
- dependencies create one session per request,
Annotatedaliases make route ownership concise and explicit.
The underlying resource and transaction rules remain in engine lifecycle, session management, and transaction boundaries.
Lifespan Ownership
Enter database_scope() once for the complete application lifecycle. Store the session factory, not the engine, because request code needs sessions rather than direct pool access:
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from fastapi import FastAPI
from .engine import database_scope
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
database_url = app.state.settings.database_url
async with database_scope(database_url) as session_factory:
app.state.session_factory = session_factory
try:
yield
finally:
del app.state.session_factory
app = FastAPI(lifespan=lifespan)
Lifespan does not construct resources per request. It enters the same framework-independent scope used by scripts, workers, and tests, keeps that scope open while requests are served, and lets it dispose the engine during shutdown.
Only store the engine too when application-level code genuinely needs direct Core operations, pool instrumentation, or engine-specific diagnostics. Routes and repositories should normally receive an AsyncSession.
Session Factory Dependency
A synchronous dependency retrieves the already-created factory from application state:
from typing import Annotated
from fastapi import Depends
from fastapi import Request
from .session import SessionFactory
from .session import transaction_scope
def get_session_factory(request: Request) -> SessionFactory:
return request.app.state.session_factory
type SessionFactoryDep = Annotated[SessionFactory, Depends(get_session_factory)]
Depends() does not create or cache a factory here. It only exposes the lifespan-owned object. This function is also the narrow seam that tests can override when they need a different factory.
Request Session Dependencies
Use a session-only dependency for reads and other request conversations that must not commit implicitly:
from collections.abc import AsyncGenerator
from sqlmodel.ext.asyncio.session import AsyncSession
async def get_session(session_factory: SessionFactoryDep) -> AsyncGenerator[AsyncSession]:
async with session_factory() as session:
yield session
type SessionDep = Annotated[AsyncSession, Depends(get_session)]
The dependency creates and closes one session per request. Closing rolls back any unfinished autobegun transaction; it does not commit.
Use a separate dependency when the whole route is one write transaction:
async def get_transaction_session(session_factory: SessionFactoryDep) -> AsyncGenerator[AsyncSession]:
async with transaction_scope(session_factory) as session:
yield session
type TransactionSessionDep = Annotated[AsyncSession, Depends(get_transaction_session)]
This adapter uses transaction_scope() from session management, so the same root transaction ownership applies inside and outside FastAPI.
Successful dependency exit commits and closes the session. Exceptional exit rolls back and closes it. Route and service code using TransactionSessionDep must not call commit(), rollback(), or close().
Route Usage
Read route:
@router.get("/items/{item_id}")
async def get_item(item_id: int, session: SessionDep) -> Item | None:
return await find_item(session, item_id)
Write route:
@router.post("/items")
async def create_item(payload: ItemCreate, session: TransactionSessionDep) -> Item:
return await insert_item(session, payload)
Choose one write convention per application:
- inject
TransactionSessionDepwhen the route itself is the complete transaction boundary, or - inject
SessionDepand placeasync with session.begin():visibly around the service call.
Do not combine both conventions in one route. Lower-level data-access functions continue to require an existing session and remain unaware of FastAPI.
Background Work
A request session belongs to that request and must not be retained by a background task. Inject or otherwise provide the application session factory, then create a new session inside the task:
async def run_background_job(session_factory: SessionFactory) -> None:
async with session_factory.begin() as session:
await process_pending_items(session)
If work must survive application shutdown, it needs an independently owned worker lifecycle rather than the FastAPI lifespan-owned factory.
Testing and Overrides
Override the narrow dependency that matches the test objective:
- Override
get_session_factoryto preserve production request-session behavior with a test factory. - Override
get_sessionwhen a test must inject one transaction-scoped session directly. - Verify each lifespan receives a fresh engine and session factory and removes application state during teardown.
- Remove overrides during teardown so mutable application state does not leak between tests.
app.dependency_overrides[get_session] = get_test_session
try:
yield app
finally:
app.dependency_overrides.pop(get_session, None)
See database testing for outer transactions, SAVEPOINT-backed fixtures, and database target selection.
Anti-Patterns
- Creating an engine or session factory in a request dependency.
- Reading settings and constructing database resources from repositories.
- Storing one mutable
AsyncSessiononapp.state. - Sharing a request session with concurrent or background tasks.
- Calling
commit()inside a route that usesTransactionSessionDep. - Keeping
app.state.session_factoryafter itsdatabase_scope()exits. - Using deprecated startup and shutdown event handlers alongside lifespan.
Integration Checklist
- Lifespan enters exactly one
database_scope()for each application lifecycle. - Application state stores the yielded session factory.
- Session dependencies create and close one session per request.
- Read and transactional dependencies have distinct commit semantics.
- Routes use
Annotatedaliases and receive sessions, not engines. - Background tasks create their own sessions from a still-live factory.
- Tests override and restore dependencies deterministically.