engine/session updates
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
# FastAPI Database Integration
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||
- [FastAPI dependencies with `yield`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/)
|
||||
- [FastAPI dependency overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/)
|
||||
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
||||
|
||||
---
|
||||
|
||||
## 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,
|
||||
- `Annotated` aliases make route ownership concise and explicit.
|
||||
|
||||
The underlying resource and transaction rules remain in [engine lifecycle](engine.md), [session management](session.md), and [transaction boundaries](transactions.md).
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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](session.md), 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:
|
||||
|
||||
```python
|
||||
@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:
|
||||
|
||||
```python
|
||||
@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 `TransactionSessionDep` when the route itself is the complete transaction boundary, or
|
||||
- inject `SessionDep` and place `async 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:
|
||||
|
||||
```python
|
||||
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_factory` to preserve production request-session behavior with a test factory.
|
||||
- Override `get_session` when 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.
|
||||
|
||||
```python
|
||||
app.dependency_overrides[get_session] = get_test_session
|
||||
try:
|
||||
yield app
|
||||
finally:
|
||||
app.dependency_overrides.pop(get_session, None)
|
||||
```
|
||||
|
||||
See [database testing](testing.md) 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 `AsyncSession` on `app.state`.
|
||||
- Sharing a request session with concurrent or background tasks.
|
||||
- Calling `commit()` inside a route that uses `TransactionSessionDep`.
|
||||
- Keeping `app.state.session_factory` after its `database_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 `Annotated` aliases and receive sessions, not engines.
|
||||
- Background tasks create their own sessions from a still-live factory.
|
||||
- Tests override and restore dependencies deterministically.
|
||||
Reference in New Issue
Block a user