SQLModel
Persistence modeling, async sessions, relationships, migrations, query loading with SQLModel and Alembic.
Boundary
Use for persistence modeling, async sessions, relationships, transactions, migrations, query optimization with SQLModel.
- pair
python for general Python conventions, tooling, FastAPI usage
- pair
arch when repo boundaries, domain separation, layering matter
- pair
design when API contract and persistence shape must stay decoupled
- pair
quality when query bugs, regressions, migration failures need tighter guards
- pair
security when persistence changes touch auth data, tenant isolation, secrets, unsafe raw SQL
Reference Map
references/advanced-models.md -- advanced model patterns, relationships, inheritance, mixins, field types, indexes, constraints
references/migrations.md -- Alembic setup, schema/data migrations, rollback patterns, prod workflow, troubleshooting
references/queries-optimization.md -- query patterns, eager loading, N+1 prevention, bulk ops, profiling, perf testing
Assets and Scripts
assets/models.py -- table, create, read, update, timestamps, enums, many-to-many patterns
scripts/init.py -- starter script to init DB from imported SQLModel metadata
scripts/migrate.sh -- migration helper wrapper around Alembic commands
What Stays Here
Defaults, async patterns, guardrails for day-to-day work.
- here: multiple-model pattern, async engine/session defaults, FastAPI dep wiring, relationship loading defaults, migration workflow, guardrails
- refs: long examples, advanced model variants, detailed migration strategies, query patterns, deeper troubleshooting
Quick Start
uv add sqlmodel sqlalchemy[asyncio] alembic
uv add asyncpg
Start with:
- explicit table, create, update, public models
- one async session per request or job boundary
- reviewed Alembic migrations
- explicit relationship loading
Multiple Model Pattern
from sqlmodel import Field, SQLModel
class UserBase(SQLModel):
name: str = Field(max_length=100)
email: str = Field(max_length=255, unique=True)
class User(UserBase, table=True):
id: int | None = Field(default=None, primary_key=True)
hashed_password: str
class UserCreate(UserBase):
password: str = Field(min_length=8)
class UserPublic(UserBase):
id: int
class UserUpdate(SQLModel):
name: str | None = None
email: str | None = None
Separate models because table shape, write input, public response rarely share responsibilities.
Async Engine and Session
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession
engine = create_async_engine(
"postgresql+asyncpg://...",
pool_size=20,
max_overflow=10,
)
async_session = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
)
expire_on_commit=False keeps returned objects usable after commit boundaries in async workflows.
FastAPI Dependency
from collections.abc import AsyncGenerator
from typing import Annotated
from fastapi import Depends
from sqlmodel.ext.asyncio.session import AsyncSession
async def get_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
yield session
SessionDep = Annotated[AsyncSession, Depends(get_session)]
One session per request or job boundary. Keep transaction scope explicit.
Relationship Defaults
from sqlalchemy.orm import selectinload
from sqlmodel import Field, Relationship, SQLModel, select
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
members: list["User"] = Relationship(back_populates="team")
statement = select(Team).options(selectinload(Team.members))
In async code, prefer explicit loading over accidental lazy loading. Default selectinload for collections unless query shape clearly favors join.
Migration Workflow
alembic init -t async alembic
alembic revision --autogenerate -m "add users table"
alembic upgrade head
Set target_metadata = SQLModel.metadata in alembic/env.py.
Review autogenerated migrations before applying. Do not trust autogenerate blindly for destructive, data-sensitive, or prod-facing changes.
Production Migration Rules
- one logical schema change per migration
- test both upgrade and downgrade when feasible
- add data migrations explicitly when schema changes need backfill or reshape
- avoid editing already-applied migrations
- stage nullable-first changes for large tables or zero-downtime rollouts
- back up prod data before risky migrations
For prod-heavy changes, load references/migrations.md.
Query and Performance Rules
- prevent N+1 with explicit eager loading
- index foreign keys and frequently filtered/ordered columns
- select only what use case needs
- batch ops over row-by-row loops when volume matters
- profile before optimizing
- keep raw SQL narrow, justified, parameterized
For deeper query patterns, load references/queries-optimization.md.
Advanced Modeling Rules
- explicit link tables for many-to-many
- mixins for reusable timestamps, soft delete, audit columns
- keep inheritance and polymorphism rare and deliberate
- keep indexes, unique constraints, cascades explicit
- separate persistence models from API/domain contracts when responsibilities differ
For advanced patterns, load references/advanced-models.md.
Guardrails
- multiple-model pattern -- never expose table models directly in responses
- always
expire_on_commit=False for async sessions unless specific reason not to
selectinload or another explicit eager-loading strategy -- prevent N+1
- validate at boundaries with create/update models
async with session.begin() for multi-step transactions
- index columns in
WHERE, ORDER BY, relationship joins
- never format SQL strings -- use query builders or parameterized SQL
- keep domain logic out of ORM models when project uses service/repository boundaries
- treat migrations as reviewed changes, not generated boilerplate
Review Focus
- persistence and public contract models separated
- async sessions have clear scope and cleanup
- no accidental lazy loading or hidden N+1
- migrations safe for real data, not schema shape
- indexes and constraints match read/write patterns
- raw SQL parameterized and justified
1---2name: sqlmodel3description: SQLModel persistence patterns for Python services and applications. Covers model design, async sessions, relationships, Alembic migrations, query optimization, N+1 prevention, and production-safe schema changes. Load when working with SQLModel, Alembic, relationships, async database layers, or SQLAlchemy-backed persistence in Python.4---56# SQLModel78Persistence modeling, async sessions, relationships, migrations, query loading with SQLModel and Alembic.910## Boundary1112Use for persistence modeling, async sessions, relationships, transactions, migrations, query optimization with SQLModel.1314- pair `python` for general Python conventions, tooling, FastAPI usage15- pair `arch` when repo boundaries, domain separation, layering matter16- pair `design` when API contract and persistence shape must stay decoupled17- pair `quality` when query bugs, regressions, migration failures need tighter guards18- pair `security` when persistence changes touch auth data, tenant isolation, secrets, unsafe raw SQL1920## Reference Map2122- `references/advanced-models.md` -- advanced model patterns, relationships, inheritance, mixins, field types, indexes, constraints23- `references/migrations.md` -- Alembic setup, schema/data migrations, rollback patterns, prod workflow, troubleshooting24- `references/queries-optimization.md` -- query patterns, eager loading, N+1 prevention, bulk ops, profiling, perf testing2526## Assets and Scripts2728- `assets/models.py` -- table, create, read, update, timestamps, enums, many-to-many patterns29- `scripts/init.py` -- starter script to init DB from imported SQLModel metadata30- `scripts/migrate.sh` -- migration helper wrapper around Alembic commands3132## What Stays Here3334Defaults, async patterns, guardrails for day-to-day work.3536- here: multiple-model pattern, async engine/session defaults, FastAPI dep wiring, relationship loading defaults, migration workflow, guardrails37- refs: long examples, advanced model variants, detailed migration strategies, query patterns, deeper troubleshooting3839## Quick Start4041```bash42uv add sqlmodel sqlalchemy[asyncio] alembic43uv add asyncpg44```4546Start with:47481. explicit table, create, update, public models492. one async session per request or job boundary503. reviewed Alembic migrations514. explicit relationship loading5253## Multiple Model Pattern5455```python56from sqlmodel import Field, SQLModel575859class UserBase(SQLModel):60 name: str = Field(max_length=100)61 email: str = Field(max_length=255, unique=True)626364class User(UserBase, table=True):65 id: int | None = Field(default=None, primary_key=True)66 hashed_password: str676869class UserCreate(UserBase):70 password: str = Field(min_length=8)717273class UserPublic(UserBase):74 id: int757677class UserUpdate(SQLModel):78 name: str | None = None79 email: str | None = None80```8182Separate models because table shape, write input, public response rarely share responsibilities.8384## Async Engine and Session8586```python87from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker88from sqlmodel.ext.asyncio.session import AsyncSession8990engine = create_async_engine(91 "postgresql+asyncpg://...",92 pool_size=20,93 max_overflow=10,94)95async_session = async_sessionmaker(96 engine,97 class_=AsyncSession,98 expire_on_commit=False,99)100```101102`expire_on_commit=False` keeps returned objects usable after commit boundaries in async workflows.103104## FastAPI Dependency105106```python107from collections.abc import AsyncGenerator108from typing import Annotated109110from fastapi import Depends111from sqlmodel.ext.asyncio.session import AsyncSession112113114async def get_session() -> AsyncGenerator[AsyncSession, None]:115 async with async_session() as session:116 yield session117118119SessionDep = Annotated[AsyncSession, Depends(get_session)]120```121122One session per request or job boundary. Keep transaction scope explicit.123124## Relationship Defaults125126```python127from sqlalchemy.orm import selectinload128from sqlmodel import Field, Relationship, SQLModel, select129130131class Team(SQLModel, table=True):132 id: int | None = Field(default=None, primary_key=True)133 name: str134 members: list["User"] = Relationship(back_populates="team")135136137statement = select(Team).options(selectinload(Team.members))138```139140In async code, prefer explicit loading over accidental lazy loading. Default `selectinload` for collections unless query shape clearly favors join.141142## Migration Workflow143144```bash145alembic init -t async alembic146alembic revision --autogenerate -m "add users table"147alembic upgrade head148```149150Set `target_metadata = SQLModel.metadata` in `alembic/env.py`.151152Review autogenerated migrations before applying. Do not trust autogenerate blindly for destructive, data-sensitive, or prod-facing changes.153154## Production Migration Rules155156- one logical schema change per migration157- test both upgrade and downgrade when feasible158- add data migrations explicitly when schema changes need backfill or reshape159- avoid editing already-applied migrations160- stage nullable-first changes for large tables or zero-downtime rollouts161- back up prod data before risky migrations162163For prod-heavy changes, load `references/migrations.md`.164165## Query and Performance Rules166167- prevent N+1 with explicit eager loading168- index foreign keys and frequently filtered/ordered columns169- select only what use case needs170- batch ops over row-by-row loops when volume matters171- profile before optimizing172- keep raw SQL narrow, justified, parameterized173174For deeper query patterns, load `references/queries-optimization.md`.175176## Advanced Modeling Rules177178- explicit link tables for many-to-many179- mixins for reusable timestamps, soft delete, audit columns180- keep inheritance and polymorphism rare and deliberate181- keep indexes, unique constraints, cascades explicit182- separate persistence models from API/domain contracts when responsibilities differ183184For advanced patterns, load `references/advanced-models.md`.185186## Guardrails187188- multiple-model pattern -- never expose table models directly in responses189- always `expire_on_commit=False` for async sessions unless specific reason not to190- `selectinload` or another explicit eager-loading strategy -- prevent N+1191- validate at boundaries with create/update models192- `async with session.begin()` for multi-step transactions193- index columns in `WHERE`, `ORDER BY`, relationship joins194- never format SQL strings -- use query builders or parameterized SQL195- keep domain logic out of ORM models when project uses service/repository boundaries196- treat migrations as reviewed changes, not generated boilerplate197198## Review Focus199200- persistence and public contract models separated201- async sessions have clear scope and cleanup202- no accidental lazy loading or hidden N+1203- migrations safe for real data, not schema shape204- indexes and constraints match read/write patterns205- raw SQL parameterized and justified