Skip to Content
React Native8. SQLite8.4 Drizzle ORM (option)

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;DRDrizzle 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 TOUT

Les gains réels, constatés :

  • Types dérivés du schéma ($inferSelect) : la version SQL brut déclare SessionRecord À 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 generate diffe le schéma TS et écrit le SQL de migration ; le hook useMigrations les 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èreSQL brut (+ lib/db)Drizzle
≤ 3-4 tables stables✅ notre cassurdimensionné
Schéma qui évolue souventmigrations manuelles OK mais attention✅ generate + types refactorés
Beaucoup d’agrégats/SQL avancé✅ le SQL est le langage naturelneutre (sql template)
Équipe/projet longdiscipline requise✅ le typage porte la discipline
UI très réactive à la baseinvalidation 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.