SQLAlchemy — Async Patterns
Async Engine & Session
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost:5432/mydb",
echo=False,
pool_size=10,
max_overflow=20,
)
AsyncSessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False,
)
Async requires an async-compatible driver: asyncpg (PostgreSQL), aiosqlite (SQLite), aiomysql (MySQL).
Basic CRUD (Async)
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
async def create_user(session: AsyncSession, name: str, email: str) -> User:
user = User(name=name, email=email)
session.add(user)
await session.flush()
return user
async def get_user(session: AsyncSession, user_id: int) -> User | None:
return await session.get(User, user_id)
async def list_users(session: AsyncSession) -> list[User]:
stmt = select(User).order_by(User.name)
result = await session.scalars(stmt)
return list(result.all())
async def update_user(session: AsyncSession, user_id: int, name: str) -> User | None:
user = await session.get(User, user_id)
if user:
user.name = name
return user
async def delete_user(session: AsyncSession, user_id: int) -> bool:
user = await session.get(User, user_id)
if user:
await session.delete(user)
return True
return False
Async Transaction Patterns
# auto-commit / auto-rollback
async with AsyncSessionLocal() as session:
async with session.begin():
session.add(User(name="Alice"))
session.add(User(name="Bob"))
# manual commit
async with AsyncSessionLocal() as session:
session.add(User(name="Alice"))
await session.commit()
Loading Strategies (Async)
Lazy loading raises MissingGreenlet in async context — always use eager loading.
from sqlalchemy import select
from sqlalchemy.orm import joinedload, selectinload
# selectinload — separate SELECT IN query (recommended for collections)
stmt = select(User).options(selectinload(User.posts))
result = await session.scalars(stmt)
users = result.all()
# joinedload — single JOIN query (good for one-to-one)
stmt = select(User).options(joinedload(User.profile))
result = await session.scalars(stmt)
user = result.first()
# nested eager loading
stmt = select(User).options(
selectinload(User.posts).selectinload(Post.comments),
)
Model-Level Default Strategy
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
posts: Mapped[list["Post"]] = relationship(
back_populates="author",
lazy="selectin", # always eager-load in async
)
FastAPI Integration (Async)
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
Use explicit transaction boundaries for writes (async with db.begin(): ...) to avoid hidden commits in read-only paths.
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
app = FastAPI()
@app.get("/users/{user_id}")
async def read_user(user_id: int, db: AsyncSession = Depends(get_db)):
user = await db.get(User, user_id)
if not user:
raise HTTPException(status_code=404)
return user
@app.get("/users")
async def list_users(db: AsyncSession = Depends(get_db)):
stmt = select(User).order_by(User.name)
result = await db.scalars(stmt)
return result.all()
AsyncAttrs Mixin
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase
class Base(AsyncAttrs, DeclarativeBase):
pass
# access relationships via awaitable_attrs (avoids MissingGreenlet)
user = await session.get(User, 1)
posts = await user.awaitable_attrs.posts
Async vs Sync Comparison
| Feature | Sync | Async |
|---|---|---|
| Engine | create_engine() |
create_async_engine() |
| Session factory | sessionmaker() |
async_sessionmaker() |
| Session class | Session |
AsyncSession |
| Get by PK | session.get(User, 1) |
await session.get(User, 1) |
| Execute | session.execute(stmt) |
await session.execute(stmt) |
| Commit | session.commit() |
await session.commit() |
| Lazy loading | Works | Raises MissingGreenlet |
| Driver (PG) | psycopg2 |
asyncpg |
Always call await engine.dispose() at shutdown to cleanly close all pooled connections.