SQLAlchemy 2.0 async
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_pathest 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.
Pourquoi expire_on_commit=False en async ?