learn.chetana.fr

SQLAlchemy 2.0 async

12 min readCore

L'ORM typé de SQLAlchemy 2.0

Le style 2.0 est dĂ©claratif ET typĂ© — mypy voit les colonnes :

class Variant(Base):
    __tablename__ = "variant"
    id:        Mapped[uuid.UUID] = mapped_column(primary_key=True)
    tenant_id: Mapped[uuid.UUID]
    name:      Mapped[str]
    price:     Mapped[float | None]          # nullable visible dans le type !

Les requĂȘtes sont des expressions select() exĂ©cutĂ©es sur une AsyncSession (driver asyncpg) :

async with session_factory() as session:
    rows = (await session.execute(
        select(Variant).where(Variant.name.ilike(f"%{q}%")).limit(20)
    )).scalars().all()

Les trois réglages du pool qui comptent

  • pool_pre_ping=True : teste la connexion avant usage — Ă©limine les « server closed the connection » aprĂšs une nuit calme ;
  • expire_on_commit=False : les objets restent lisibles aprĂšs commit (sinon chaque accĂšs post-commit refait un SELECT surprise — piĂšge n°1 en async) ;
  • le search_path est passĂ© au driver au dĂ©marrage : le schĂ©ma (copilot) n'est jamais Ă©crit en dur dans les requĂȘtes — il vient de la config (DJUST_DB_SCHEMA).

Et le piĂšge async classique : le lazy loading implicite des relations ne fonctionne pas en async (pas d'I/O implicite possible) — les relations se chargent explicitement (selectinload), ce qui a le mĂ©rite de rendre les N+1 visibles au lieu de silencieux.

Les migrations : du SQL nu, discipliné

Pas d'Alembic : un runner maison applique des fichiers SQL horodatĂ©s (202605060903_variant.sql), chacun idempotent (CREATE TABLE IF NOT EXISTS, DROP POLICY IF EXISTS avant CREATE POLICY) avec SET LOCAL lock_timeout pour ne jamais bloquer la prod, et un linter SQL (squawk) en CI. La philosophie : les features PostgreSQL utilisĂ©es (RLS, colonnes gĂ©nĂ©rĂ©es, HNSW
) sont trop spĂ©cifiques pour une couche d'abstraction — le SQL est assumĂ© comme code de premier rang.

đŸ§© Quiz1/3

Pourquoi expire_on_commit=False en async ?

🃏 Flashcards1/4