Files
2026-06-01 21:42:05 +00:00

128 lines
4.1 KiB
Markdown

# Persistence
Keep session lifetime and write boundaries explicit. Route handlers can orchestrate straightforward database work, but persistence concerns should stay visible instead of hiding behind ambient sessions or magical commits.
## Why this convention exists
FastAPI apps often stay clean until persistence gets sloppy. Then you get:
- hidden session creation inside helpers
- unclear transaction boundaries
- handlers packed with ORM choreography
- tests that only pass because the database layer was mocked out of existence
The stronger pattern in mature examples is straightforward:
- sessions arrive through dependencies
- read and write boundaries are visible in the calling flow
- sync and async persistence are deliberate choices
- tests exercise real session lifetime and cleanup
## The convention
1. Provide sessions through dependencies.
2. Keep transaction and commit boundaries visible.
3. Make sync versus async persistence an explicit stack decision.
4. Let routes do simple persistence orchestration, but move reusable business rules out once the flow stops being trivial.
5. Test against real persistence seams when practical.
## Follow it when stored state matters
Typical fits:
- routes that read or write the database
- flows where transaction scope matters
- auth-aware queries
- cleanup-sensitive tests
## Avoid these mistakes
Do *not* invent a repository or service layer for every one-line query.
Do *not* hide session creation deep inside helper functions.
Do *not* force async persistence just because the web layer is async. Choose sync or async deliberately based on the real stack.
## Preferred shapes
### Session through dependency
```python
def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
```
This makes resource lifetime explicit and testable.
### Visible write boundary
```python
item = Item.model_validate(item_in, update={"owner_id": current_user.id})
session.add(item)
session.commit()
session.refresh(item)
return item
```
This is honest about where state changes happen.
### Explicit partial-update flow
```python
update_dict = item_in.model_dump(exclude_unset=True)
item.sqlmodel_update(update_dict)
session.add(item)
session.commit()
session.refresh(item)
```
Again: no hidden writes, no mystery transaction.
## Counterexamples
### Hidden ambient session access
```python
def create_item(item_in: ItemCreate) -> Item:
session = Session(engine)
...
```
Bad because session lifetime is buried in the wrong layer.
### Commit hidden deep in helpers
If callers cannot tell where the write boundary is, transaction reasoning gets ugly fast.
### Mock-only persistence tests
If no test ever exercises real session lifetime, query shape, or cleanup behavior, the persistence layer will surprise you in production.
## Source signals
### Full-stack FastAPI Template
- `backend/app/api/deps.py:21-27` provides DB access through a yielded session dependency.
- `backend/app/api/routes/items.py:21-45` keeps read query shape explicit in the route, including auth-aware filtering and response shaping.
- `backend/app/api/routes/items.py:68-72` shows a visible create, commit, and refresh boundary.
- `backend/app/api/routes/items.py:86-95` shows the same visibility on update.
- `backend/app/api/routes/items.py:106-112` shows delete followed by an explicit commit.
- `backend/tests/conftest.py:15-24` keeps test session lifetime and cleanup explicit.
### SQLAlchemy and Starlette lifespan
- `examples/asyncio/async_orm.py:63-104` in SQLAlchemy shows explicit async session and transaction handling instead of hidden ambient state.
- `examples/inheritance/joined.py:93-120` shows the sync equivalent with visible `Session(...)` and `commit()` boundaries.
- `starlette/applications.py:46-48` reinforces that long-lived app resources belong in lifespan rather than being initialized lazily inside request-time persistence helpers.
## Bottom line
Persistence should be visible.
Sessions should come from clear boundaries.
Transactions should not be magical.
If a reader cannot tell where data is loaded, changed, and committed, the persistence design needs work.