Initial extracted documentation set
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user