improving async fastapi sqlmodel skill
This commit is contained in:
@@ -1,53 +1,50 @@
|
||||
# SQLModel Adoption and Boundaries
|
||||
# SQLModel-First Modeling and Async Boundaries
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [SQLModel documentation](https://sqlmodel.tiangolo.com/)
|
||||
- [SQLModel features](https://sqlmodel.tiangolo.com/features/)
|
||||
- [SQLModel advanced guide](https://sqlmodel.tiangolo.com/advanced/)
|
||||
- [SQLModel FastAPI session dependency tutorial](https://sqlmodel.tiangolo.com/tutorial/fastapi/session-with-dependency/)
|
||||
- [SQLModel release notes](https://sqlmodel.tiangolo.com/release-notes/)
|
||||
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
||||
|
||||
??? abstract "Decision metadata"
|
||||
- Status: adopted
|
||||
- Decision level: advisory
|
||||
- Decision level: mandatory
|
||||
- Applies to: api-runtime, workers, tests
|
||||
- Last reviewed: 2026-06-26
|
||||
- Last reviewed: 2026-07-26
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Define when and how to use SQLModel in an async FastAPI + SQLAlchemy modernization effort.
|
||||
Define SQLModel as the primary model layer for async FastAPI applications and explain how it composes with SQLAlchemy's async runtime.
|
||||
|
||||
The goal is pragmatic adoption: use SQLModel where it reduces model duplication and improves typing ergonomics, without disrupting established async engine/session lifecycle rules.
|
||||
SQLModel is designed for FastAPI, built on Pydantic and SQLAlchemy, and intended to minimize duplication while preserving the capabilities of both. Async engine, session, transaction, and loading behavior still follow SQLAlchemy's asyncio contract.
|
||||
|
||||
---
|
||||
|
||||
## Scope and Non-Goals
|
||||
|
||||
- In scope: model-layer decisions, integration boundaries, phased adoption strategy.
|
||||
- Out of scope: full framework rewrites and all-at-once model migration.
|
||||
- In scope: table models, API data models, SQLAlchemy interoperability, async session usage, and exception criteria.
|
||||
- Out of scope: replacing SQLAlchemy's async runtime primitives or claiming that synchronous tutorial examples are async patterns.
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Default to SQLModel for new table models and API data models.
|
||||
- Keep SQLAlchemy async primitives as the runtime base: `create_async_engine`, `async_sessionmaker`, and `AsyncSession`.
|
||||
- Prefer SQLModel for new domain modules where table models and API schemas would otherwise be duplicated.
|
||||
- Migrate by bounded module or feature area; do not force whole-repo conversion in one phase.
|
||||
- Keep transaction and session ownership policies identical whether models are SQLAlchemy Declarative or SQLModel.
|
||||
- Document explicit reasons when SQLModel is deferred for a module.
|
||||
- Use SQLModel inheritance to share validated fields while keeping table, create, update, and public contracts distinct where their semantics differ.
|
||||
- Use SQLAlchemy declarative models only for a concrete unsupported mapping or third-party constraint; document the reason.
|
||||
- Use SQLAlchemy relationship loading options explicitly on async paths.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Patterns
|
||||
|
||||
### Pattern A: Bounded module adoption
|
||||
|
||||
- Choose one feature slice (for example, billing, projects, or auth profile data).
|
||||
- Introduce SQLModel models for that slice only.
|
||||
- Keep unchanged modules on existing SQLAlchemy models until a dedicated migration phase.
|
||||
|
||||
### Pattern B: Data model split for API boundaries
|
||||
### Pattern A: Data model split for API boundaries
|
||||
|
||||
Use distinct models for persistence and external contracts.
|
||||
|
||||
@@ -72,20 +69,29 @@ class UserRead(UserBase):
|
||||
id: int
|
||||
```
|
||||
|
||||
### Pattern C: Keep async lifecycle unchanged
|
||||
### Pattern B: Keep SQLModel models with the async runtime
|
||||
|
||||
```python
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
from sqlmodel import select
|
||||
|
||||
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
|
||||
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with session_factory() as session:
|
||||
users = (await session.scalars(select(User))).all()
|
||||
```
|
||||
|
||||
`sqlmodel.select()` keeps SQLModel's typing-oriented statement construction, while `AsyncSession.scalars()` and the surrounding lifecycle come from SQLAlchemy.
|
||||
|
||||
---
|
||||
|
||||
## Interoperability Notes
|
||||
|
||||
- SQLModel is designed as a thin layer over SQLAlchemy and Pydantic, so mixed codebases are expected during migration.
|
||||
- A SQLModel table model is a SQLAlchemy model and can participate in SQLAlchemy relationships, statements, loader options, and sessions.
|
||||
- A SQLModel model is also a Pydantic model; non-table models are useful for request and response contracts.
|
||||
- SQLModel's official FastAPI dependency tutorial currently uses synchronous `Session`; translate the ownership pattern, not the concrete session type, for async applications.
|
||||
- SQLModel's advanced guide still lists dedicated async documentation as future work, so use SQLAlchemy's asyncio documentation as the authority for runtime mechanics.
|
||||
- Prefer one query style per module to reduce cognitive overhead.
|
||||
- Keep loader strategies explicit in async paths to avoid implicit I/O surprises.
|
||||
|
||||
@@ -93,31 +99,29 @@ session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_comm
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Treating SQLModel adoption as equivalent to async-session modernization.
|
||||
- Rewriting all models at once without rollback checkpoints.
|
||||
- Introducing SQLModel in handlers while keeping old global/shared session patterns.
|
||||
- Treating SQLModel as an alternative to SQLAlchemy rather than a layer built on it.
|
||||
- Copying a synchronous `Session` example into an async request path.
|
||||
- Constructing sessions in handlers instead of using the application session factory.
|
||||
- Mixing multiple query/session idioms within the same module without clear conventions.
|
||||
|
||||
---
|
||||
|
||||
## Operational Checks
|
||||
|
||||
- Modernized module documents whether it is SQLModel-first or SQLAlchemy-only.
|
||||
- New model modules are SQLModel-first; exceptions state the unsupported need or constraint.
|
||||
- Session/transaction ownership remains consistent across both model styles.
|
||||
- New model modules use explicit API boundary models where needed.
|
||||
- Table, create, update, and public models share fields intentionally without exposing persistence-only data.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checks
|
||||
|
||||
- Module-level tests verify CRUD semantics for adopted SQLModel models.
|
||||
- Module-level tests verify CRUD semantics for SQLModel models through `AsyncSession`.
|
||||
- API tests verify response/request model behavior for SQLModel-based endpoints.
|
||||
- Regression tests confirm unchanged modules continue to function during phased rollout.
|
||||
- Relationship tests verify async loader strategies do not depend on implicit I/O.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
## Version Checks
|
||||
|
||||
- Start with low-risk bounded domains.
|
||||
- Expand only after validation of session lifecycle, transaction behavior, and endpoint correctness.
|
||||
- Maintain a tracked backlog of deferred modules with rationale and planned phase.
|
||||
- Verify installed SQLModel, SQLAlchemy, and Pydantic versions together when using newly added typing or ORM features.
|
||||
Reference in New Issue
Block a user