Chapitre 8.4 — Drizzle ORM (option)
Où on en est : Drill tourne en SQL brut, propre et suffisant. Ce chapitre est un panorama honnête de l’étage au-dessus — Drizzle — pour décider en connaissance de cause dans VOS futures apps.
⏱️ TL;DR — Drizzle apporte : schéma déclaré en TS (types dérivés automatiquement), requêtes typées de bout en bout, migrations générées par
drizzle-kit, et live queries (useLiveQuery: l’UI suit la base). Coût : une couche d’abstraction, une config, et du SQL qu’on écrit moins (donc qu’on lit moins bien). Verdict du cours : Drill reste en SQL brut ; Drizzle devient rentable quand le schéma dépasse ~5 tables ou change souvent.
🎯 Objectifs
- Voir à quoi ressemble Drizzle sur le schéma de Drill (comparaison directe).
- Comprendre ce que les live queries changent au modèle Query/invalidation.
- Poser les critères de décision SQL brut vs ORM pour vos apps.
Le schéma Drill, version Drizzle
pnpm add drizzle-orm
pnpm add -D drizzle-kit// db/schema.ts — le schéma EST du TypeScript
import { sqliteTable, integer, text, index } from 'drizzle-orm/sqlite-core'
export const sessions = sqliteTable('sessions', {
id: integer('id').primaryKey({ autoIncrement: true }),
theme: text('theme').notNull(),
score: integer('score').notNull(),
total: integer('total').notNull(),
correctCount: integer('correct_count').notNull(),
bestStreak: integer('best_streak').notNull(),
playedAt: integer('played_at').notNull(),
}, t => [index('idx_sessions_theme_date').on(t.theme, t.playedAt)])
export const answers = sqliteTable('answers', {
id: integer('id').primaryKey({ autoIncrement: true }),
sessionId: integer('session_id').notNull()
.references(() => sessions.id, { onDelete: 'cascade' }),
questionId: text('question_id').notNull(),
correct: integer('correct', { mode: 'boolean' }).notNull(), // ← 0/1 ↔ boolean AUTO
})
export type Session = typeof sessions.$inferSelect // le type sort du schéma
export type NewSession = typeof sessions.$inferInsert// Usage — requêtes typées, autocomplétées, refactorables
import { drizzle } from 'drizzle-orm/expo-sqlite'
import { desc, eq } from 'drizzle-orm'
const db = drizzle(useSQLiteContext())
const history = await db.select().from(sessions)
.where(eq(sessions.theme, 'symfony'))
.orderBy(desc(sessions.playedAt))
.limit(20)
// history: Session[] — typé sans annotation, renommer une colonne refactore TOUTLes gains réels, constatés :
- Types dérivés du schéma (
$inferSelect) : la version SQL brut déclareSessionRecordÀ CÔTÉ du CREATE TABLE — deux vérités qui peuvent diverger. Ici, une seule. - Conversions automatiques :
mode: 'boolean',mode: 'timestamp'— nos 0/1 et epoch ms manuels, gérés. - Migrations générées :
drizzle-kit generatediffe le schéma TS et écrit le SQL de migration ; le hookuseMigrationsles applique au démarrage (notre moteur user_version, industrialisé). - Live queries :
useLiveQuery(db.select()…)— le composant se re-rend quand la table change (via l’API de change-notification de SQLite). L’invalidation manuelle du chapitre 8.3 devient inutile pour ces vues.
Ce que ça coûte
- Une couche à apprendre et à déboguer : quand une requête générée surprend, il faut
savoir lire le SQL émis (
.toSQL()) — l’ORM n’exonère jamais de comprendre SQLite. - Les agrégats restent du quasi-SQL : notre getWorstQuestions en Drizzle s’écrit avec
sql\…“ template ou l’API d’agrégation — gain mince précisément là où Drill a ses requêtes les plus intéressantes. - De la config (babel plugin pour les migrations bundlées, drizzle.config.ts) et une dep de plus à suivre à chaque montée de SDK.
La grille de décision
| Critère | SQL brut (+ lib/db) | Drizzle |
|---|---|---|
| ≤ 3-4 tables stables | ✅ notre cas | surdimensionné |
| Schéma qui évolue souvent | migrations manuelles OK mais attention | ✅ generate + types refactorés |
| Beaucoup d’agrégats/SQL avancé | ✅ le SQL est le langage naturel | neutre (sql template) |
| Équipe/projet long | discipline requise | ✅ le typage porte la discipline |
| UI très réactive à la base | invalidation Query (très bien) | ✅ live queries |
Verdict Drill : SQL brut — 2 tables, schéma stable, agrégats au cœur. Verdict pour votre future app de suivi de dépenses (P19) avec comptes/catégories/budgets/récurrences : Drizzle d’entrée. La compétence à avoir : les DEUX, et le discernement.
💡 Pour un dev Next.js — C’est le même Drizzle que côté serveur (Postgres/Vercel) — une compétence, deux mondes. Si vous êtes plutôt Prisma : Prisma supporte RN/Expo mais reste plus lourd ; l’écosystème mobile a majoritairement choisi Drizzle pour son poids plume et son mode synchrone SQLite.
📚 Aller plus loin — La doc « Drizzle with Expo SQLite » (guide officiel, migrations et live queries), et le talk « SQLite is all you need » — le mouvement local-first dont Drill est un petit citoyen.
✏️ Exercices
1. Sans rien installer : réécrivez mentalement (ou sur papier) insertSession + son transaction en Drizzle. Qu’est-ce qui disparaît, qu’est-ce qui reste ?
✅ Solution
await db.transaction(async tx => {
const [s] = await tx.insert(sessions).values(newSession).returning({ id: sessions.id })
await tx.insert(answers).values(answerRows.map(a => ({ ...a, sessionId: s.id })))
})Disparaissent : le SQL string, lastInsertRowId manuel, la conversion 0/1, l’insertion en boucle (values accepte un tableau). Restent : la transaction (le concept ne s’abstrait pas) et la structure du flux. Un ORM change l’écriture, pas l’architecture.
2. Listez les 3 signaux qui, dans une de VOS futures apps, déclencheraient la migration vers Drizzle.
✅ Solution
(1) La 4e-5e table arrive (jointures fréquentes, types manuels qui prolifèrent) ; (2) le schéma change chaque semaine (générer > écrire les migrations) ; (3) des bugs de désynchronisation type TS ↔ colonnes réelles (le signal qu’une seule source de vérité manque). Bonus : besoin d’UI temps réel sur la base → live queries.
3. Le mode ‘boolean’ de Drizzle stocke toujours 0/1 en base. Vérifiez l’affirmation avec .toSQL() (ou en raisonnant) et expliquez pourquoi c’est important pour une migration future SQL brut → Drizzle.
✅ Solution
SQLite n’a pas de type booléen : mode: 'boolean' est une conversion à la frontière
(INTEGER 0/1 en base ↔ boolean en TS) — exactement ce que notre lib/db faisait à la main. La
représentation disque étant identique, adopter Drizzle sur une base existante ne demande
AUCUNE migration de données : seules les couches de code changent. Les bons ORM ne réinventent
pas le stockage.
🧠 Quiz
1. D’où sortent les types Session/NewSession en Drizzle ?
Réponse
Du schéma lui-même : typeof sessions.$inferSelect / $inferInsert — une seule source de
vérité, le type suit toute modification de colonne.
2. Que fait useLiveQuery, et que remplace-t-il dans notre architecture ?
Réponse
Il re-rend le composant quand les données de la requête changent en base — remplaçant l’invalidation manuelle de TanStack Query pour ces vues locales.
3. Citez deux conversions automatiques de Drizzle utiles en mobile.
Réponse
integer(…, { mode: 'boolean' }) (0/1 ↔ boolean) et mode: 'timestamp' (epoch ↔ Date) —
les frontières qu’on codait à la main en lib/db.
4. Pourquoi le cours garde-t-il Drill en SQL brut ?
Réponse
2 tables, schéma stable, et le cœur du domaine est fait d’agrégats — là où l’ORM apporte le moins. La grille : peu de tables stables → brut ; schéma riche et mouvant → Drizzle.
5. Un ORM dispense-t-il d’apprendre SQL ?
Réponse
Non : il faut lire le SQL émis pour déboguer/optimiser, et les requêtes non triviales (agrégats, HAVING) restent du SQL à peine déguisé. L’ORM industrialise, il ne remplace pas la compréhension.
👉 Partie suivante : Partie 9 — Les APIs natives d’Expo — notifications du défi quotidien, haptics, sons, partage : Drill devient une app qui vit dans le téléphone.