data / sqlalchemy
SQLAlchemy async
Explication
Ce que vous allez apprendre
- Comprendre pourquoi l'asynchrone rend du temps à l'application pendant une attente d'I/O
- Créer un moteur et une session async avec
create_async_engine/AsyncSession - Ajouter
awaitsur chaque opération qui touche la base (execute,commit,get) - Comprendre pourquoi l'eager loading devient presque obligatoire en async
- Éviter de partager une
AsyncSessionentre plusieurs tâchesasyncioexécutées en parallèle
Dans quel contexte ?
Une API FastAPI à fort trafic sert des dizaines de requêtes simultanées, chacune interrogeant la base de données. En mode synchrone, chaque requête bloque un worker entier pendant l'attente de la réponse SQL, ce qui limite le nombre de requêtes traitées en parallèle aux nombres de workers disponibles. Passer à AsyncSession permet à un même worker de traiter d'autres requêtes pendant qu'il attend la base, sans changer la logique métier.
D'abord, le problème du temps d'attente bloqué
Dans un serveur web classique (synchrone), pendant qu'une requête attend une réponse de la base de données, tout le "worker" reste bloqué et ne peut traiter aucune autre demande pendant ce temps.
La solution : rendre ce temps mort à l'application
En asynchrone, ce temps d'attente est rendu à l'application, qui peut traiter d'autres requêtes entre-temps. Pour des applications à fort trafic avec beaucoup d'opérations d'entrée-sortie, cela permet de servir bien plus de requêtes avec les mêmes ressources.
Étape 1 : ce qui change concrètement dans le code
La version async (AsyncSession) reprend exactement les mêmes concepts que la session synchrone — unit of work, identity map, select(). La différence tient en un mot-clé : chaque opération qui touche la base (execute, commit, get) doit être précédée d'await, qui signale à Python "cette opération peut prendre du temps, laisse la main à d'autres tâches pendant ce temps".
| Synchrone | Async |
|---|---|
Session | AsyncSession |
session.execute(stmt) | await session.execute(stmt) |
session.commit() | await session.commit() |
| accès lazy à une relation possible hors session | lève une erreur : eager loading requis |
Un piège spécifique à l'async
En mode synchrone, un accès lazy à une relation non chargée (leçon 7) déclenche simplement une requête SQL supplémentaire, même après coup. En async, ce n'est pas possible : await ne peut pas être inséré automatiquement au milieu d'un accès normal à un attribut Python.
La conséquence directe
Tenter d'accéder à une relation lazy hors session lève une erreur en async. L'eager loading (selectinload, vu en leçon 7) devient donc presque obligatoire, pas seulement recommandé pour la performance comme en synchrone.
Un dernier piège à connaître
Une AsyncSession ne peut pas être partagée entre plusieurs tâches asyncio exécutées en parallèle (asyncio.gather) : chacune doit ouvrir la sienne, sous peine de corruption d'état interne.
Piège fréquent
Passer la MÊME instance d'AsyncSession à plusieurs coroutines lancées avec asyncio.gather() corrompt son état interne, car une session n'est pas conçue pour un accès concurrent. Ouvrez systématiquement une session distincte par tâche asynchrone.
Vers la suite
Après avoir vu comment exécuter des requêtes efficacement, la prochaine leçon change d'échelle : comment structurer tout un projet pour que la logique métier ne dépende plus directement de SQLAlchemy.
Commandes & code
SQLAlchemy async
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload
# Driver async : asyncpg pour PostgreSQL, aiosqlite pour SQLite
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/technologik")
AsyncSessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
async def creer_utilisateur(email: str, nom: str) -> Utilisateur:
async with AsyncSessionLocal() as session:
utilisateur = Utilisateur(email=email, nom=nom)
session.add(utilisateur)
await session.commit()
await session.refresh(utilisateur) # recharge les champs générés côté base
return utilisateur
async def lister_actifs() -> list[Utilisateur]:
async with AsyncSessionLocal() as session:
stmt = select(Utilisateur).where(Utilisateur.est_actif == True)
resultat = await session.execute(stmt)
return list(resultat.scalars().all())
# Eager loading obligatoire en async : accéder à une relation lazy hors session lève une erreur
async def lister_auteurs_avec_articles() -> list[Auteur]:
async with AsyncSessionLocal() as session:
stmt = select(Auteur).options(selectinload(Auteur.articles))
resultat = await session.execute(stmt)
return list(resultat.scalars().all())
# Dependency FastAPI async
from collections.abc import AsyncGenerator
async def get_async_db() -> AsyncGenerator[AsyncSession, None]:
async with AsyncSessionLocal() as session:
yield session
# @app.get("/utilisateurs")
# async def route(db: AsyncSession = Depends(get_async_db)):
# resultat = await db.execute(select(Utilisateur))
# return resultat.scalars().all()
# Requêtes concurrentes : chaque tâche a besoin de SA PROPRE session (non partageable)
async def charger_en_parallele():
async def charger_utilisateur(uid: int):
async with AsyncSessionLocal() as session:
return await session.get(Utilisateur, uid)
resultats = await asyncio.gather(
charger_utilisateur(1),
charger_utilisateur(2),
charger_utilisateur(3),
)
return resultats
# Transaction async explicite
async def transferer(source_id: int, dest_id: int, montant: float):
async with AsyncSessionLocal() as session:
async with session.begin():
source = await session.get(Compte, source_id)
dest = await session.get(Compte, dest_id)
source.solde -= montant
dest.solde += montant
if source.solde < 0:
raise ValueError("Solde insuffisant") # déclenche un rollback automatique
# run_sync : exécuter du code synchrone (legacy) dans un contexte async
async def utiliser_api_synchrone():
async with AsyncSessionLocal() as session:
def operation_sync(sync_session):
return sync_session.execute(select(Utilisateur)).scalars().all()
return await session.run_sync(operation_sync)Résumé
create_async_engine+AsyncSessionnécessitent un driver async (asyncpg,aiosqlite).- Toute requête (
execute,commit,get) doit êtreawait-ée ; l'accès lazy à une relation hors session échoue -- charger en eager (selectinload). - Chaque tâche
asyncioconcurrente doit ouvrir sa propreAsyncSession: une session n'est pas partageable entre coroutines. session.run_sync(fn)permet d'exécuter ponctuellement du code utilisant l'API synchrone dans un contexte async.
Exercices pratiques
Mission : une AsyncSession partagée qui corrompt des requêtes parallèles
Objectif : Diagnostiquer un bug de corruption causé par le partage d'une AsyncSession entre coroutines, et le corriger.
Contexte
Un développeur écrit une fonction charger_en_parallele qui ouvre UNE SEULE AsyncSession, puis la passe à trois coroutines lancées avec asyncio.gather() pour charger trois utilisateurs "en parallèle". En test manuel isolé ça semble marcher, mais sous charge réelle, l'application lève des erreurs internes incompréhensibles et retourne parfois des données mélangées entre utilisateurs.