Cours Next.js · Module 4: Bases de données et accès aux données
Modèles de données et migrations
Dans cette leçon6
Dans les modules précédents, nous avons abordé diverses bases de données et différents ORM pour les applications Next.js. Nous allons maintenant nous concentrer sur un aspect clé du travail avec les bases : créer des modèles de données et gérer les migrations. Ces compétences sont essentielles pour construire des applications scalables dotées d'une couche de données bien conçue.
Concevoir des modèles de données
Une bonne conception des modèles de données est le socle d'une application stable. Des modèles mal conçus peuvent entraîner des problèmes de performance, des difficultés de maintenance et des limites de scalabilité.
Les principes de conception des modèles
- Normalisation - Évite la duplication des données
- Clés non ambiguës - Chaque enregistrement doit avoir un identifiant unique
- Intégrité référentielle - Les relations entre tables doivent être clairement définies
- Économie d'attributs - Ne crée pas de champs inutiles
- Types de données appropriés - Choisis les types optimaux pour chaque champ
Exemple de schéma pour une application e-commerce
Voici un exemple de schéma pour une application e-commerce simple :
1// Prisma schema
2model User {
3 id String @id @default(cuid())
4 email String @unique
5 name String?
6 password String
7 role Role @default(CUSTOMER)
8 orders Order[]
9 shippingAddresses Address[] @relation("UserShippingAddresses")
10 createdAt DateTime @default(now())
11 updatedAt DateTime @updatedAt
12}
13
14model Product {
15 id String @id @default(cuid())
16 name String
17 description String?
18 price Decimal @db.Decimal(10, 2)
19 inventory Int @default(0)
20 images String[]
21 categories Category[]
22 orderItems OrderItem[]
23 createdAt DateTime @default(now())
24 updatedAt DateTime @updatedAt
25}
26
27model Category {
28 id String @id @default(cuid())
29 name String @unique
30 description String?
31 products Product[]
32 parentCategory Category? @relation("SubCategories", fields: [parentId], references: [id])
33 parentId String?
34 subCategories Category[] @relation("SubCategories")
35 createdAt DateTime @default(now())
36 updatedAt DateTime @updatedAt
37}
38
39model Order {
40 id String @id @default(cuid())
41 user User @relation(fields: [userId], references: [id])
42 userId String
43 status OrderStatus @default(PENDING)
44 items OrderItem[]
45 totalAmount Decimal @db.Decimal(10, 2)
46 shippingAddress Address @relation(fields: [addressId], references: [id])
47 addressId String
48 paymentIntent String?
49 createdAt DateTime @default(now())
50 updatedAt DateTime @updatedAt
51}
52
53model OrderItem {
54 id String @id @default(cuid())
55 order Order @relation(fields: [orderId], references: [id])
56 orderId String
57 product Product @relation(fields: [productId], references: [id])
58 productId String
59 quantity Int
60 priceAtPurchase Decimal @db.Decimal(10, 2)
61}
62
63model Address {
64 id String @id @default(cuid())
65 street String
66 city String
67 state String
68 zipCode String
69 country String
70 user User @relation("UserShippingAddresses", fields: [userId], references: [id])
71 userId String
72 orders Order[]
73 isDefault Boolean @default(false)
74 createdAt DateTime @default(now())
75 updatedAt DateTime @updatedAt
76}
77
78enum Role {
79 ADMIN
80 STAFF
81 CUSTOMER
82}
83
84enum OrderStatus {
85 PENDING
86 PROCESSING
87 SHIPPED
88 DELIVERED
89 CANCELLED
90}Les relations entre modèles
Dans la conception d'une base de données, les relations entre modèles sont cruciales. On distingue trois grands types de relations :
Un-à-un (1:1)
- Chaque enregistrement de la table A est associé à exactement un enregistrement de la table B
- Exemple : User et Profile
1model User { 2 id String @id @default(cuid()) 3 profile Profile? 4} 5 6model Profile { 7 id String @id @default(cuid()) 8 user User @relation(fields: [userId], references: [id]) 9 userId String @unique // @unique garantit une relation 1:1 10 bio String? 11}Un-à-plusieurs (1:N)
- Un enregistrement de la table A peut être associé à plusieurs enregistrements de la table B
- Exemple : User et Orders
1model User { 2 id String @id @default(cuid()) 3 orders Order[] 4} 5 6model Order { 7 id String @id @default(cuid()) 8 user User @relation(fields: [userId], references: [id]) 9 userId String 10}Plusieurs-à-plusieurs (N:M)
- Plusieurs enregistrements de la table A peuvent être associés à plusieurs enregistrements de la table B
- Exemple : Products et Categories
1model Product { 2 id String @id @default(cuid()) 3 categories Category[] 4} 5 6model Category { 7 id String @id @default(cuid()) 8 products Product[] 9}
Les migrations de base de données
Les migrations sont un mécanisme qui permet de modifier la structure de la base de manière contrôlée. Elles permettent de suivre l'historique des changements, de collaborer en équipe et de déployer les mises à jour de schéma en toute sécurité.
Que sont les migrations ?
Les migrations sont des fichiers contenant des changements du schéma de la base (création de tables, ajout de colonnes, modification de relations, etc.). Chaque migration représente un changement unique et atomique de la structure de la base.
Pourquoi utiliser des migrations ?
- Historique des changements - Documentation complète de l'évolution de la structure de la base
- Collaboration en équipe - Facilite le travail de plusieurs développeurs sur un même projet
- Versionnement - Intégration avec les systèmes de contrôle de version
- Déploiement sûr - Tester les changements avant la production
- Rollback - Possibilité d'annuler les changements en cas de problème
Les migrations dans Prisma
Prisma propose un système de migration avancé, bien intégré à la modélisation des données.
Créer la première migration
1# Initialiser Prisma (si ce n'est pas encore fait)
2npx prisma init
3
4# Après avoir défini les modèles dans schema.prisma
5npx prisma migrate dev --name initCette commande :
- Analyse le fichier
schema.prisma - Génère un fichier SQL avec les instructions de création de tables
- Applique la migration à la base de données
- Génère le client Prisma
Faire évoluer le schéma et les migrations suivantes
Après avoir modifié le schéma, on peut générer une nouvelle migration :
1# Après avoir modifié schema.prisma
2npx prisma migrate dev --name add_user_preferencesDéployer les migrations en production
En environnement de production, on utilise une commande différente qui applique uniquement les migrations sans générer le client :
1npx prisma migrate deployRéinitialiser la base de données (développement uniquement)
En environnement de développement, il faut parfois réinitialiser la base :
1npx prisma migrate resetCette commande :
- Supprime toute la base de données
- Crée une nouvelle base vide
- Applique toutes les migrations
- Exécute les seeders (s'ils sont définis)
Les migrations dans Drizzle
Drizzle propose également un système de migration, mais basé sur des fichiers SQL.
Générer les migrations
1# Configuration drizzle.config.ts
2#
3# Génération des migrations
4npx drizzle-kit generate:pgCette commande génère des fichiers de migration SQL à partir des définitions du schéma.
Exécuter les migrations
1// migrations.ts
2import { migrate } from 'drizzle-orm/node-postgres/migrator';
3import { db } from './db';
4
5async function main() {
6 await migrate(db, { migrationsFolder: './drizzle' });
7 console.log('Migrations terminées');
8}
9
10main().catch((e) => {
11 console.error('Échec des migrations');
12 console.error(e);
13 process.exit(1);
14});Les migrations dans Sequelize
Sequelize propose un système de migration basé sur des fichiers JavaScript/TypeScript.
Initialiser les migrations
1# Installation de la CLI Sequelize
2npm install -g sequelize-cli
3
4# Initialisation de la structure du projet
5npx sequelize-cli initCela crée la structure suivante :
config/- configuration de la base de donnéesmigrations/- fichiers de migrationmodels/- modèles Sequelizeseeders/- données initiales
Créer une migration
1npx sequelize-cli migration:generate --name create-usersCela crée le squelette d'un fichier de migration :
1// migrations/YYYYMMDDHHMMSS-create-users.js
2'use strict';
3
4/** @type {import('sequelize-cli').Migration} */
5module.exports = {
6 async up(queryInterface, Sequelize) {
7 await queryInterface.createTable('Users', {
8 id: {
9 allowNull: false,
10 autoIncrement: true,
11 primaryKey: true,
12 type: Sequelize.INTEGER
13 },
14 name: {
15 type: Sequelize.STRING
16 },
17 email: {
18 type: Sequelize.STRING,
19 unique: true
20 },
21 createdAt: {
22 allowNull: false,
23 type: Sequelize.DATE
24 },
25 updatedAt: {
26 allowNull: false,
27 type: Sequelize.DATE
28 }
29 });
30 },
31
32 async down(queryInterface, Sequelize) {
33 await queryInterface.dropTable('Users');
34 }
35};Exécuter les migrations
1# Exécute toutes les migrations en attente
2npx sequelize-cli db:migrate
3
4# Annule la dernière migration
5npx sequelize-cli db:migrate:undo
6
7# Annule toutes les migrations
8npx sequelize-cli db:migrate:undo:allTechniques de migration avancées
Le seeding de données
Le seeding consiste à peupler la base avec des données initiales, ce qui est utile pour les tests et le développement.
Le seeding dans Prisma
1// prisma/seed.ts
2import { PrismaClient } from '@prisma/client';
3
4const prisma = new PrismaClient();
5
6async function main() {
7 // Création des catégories
8 const electronicsCategory = await prisma.category.upsert({
9 where: { name: 'Electronics' },
10 update: {},
11 create: {
12 name: 'Electronics',
13 description: 'Appareils et accessoires électroniques'
14 },
15 });
16
17 // Création des produits
18 const product1 = await prisma.product.upsert({
19 where: { id: 'prod_1' },
20 update: {},
21 create: {
22 id: 'prod_1',
23 name: 'Smartphone X',
24 description: 'Dernier modèle avec des fonctionnalités avancées',
25 price: 999.99,
26 inventory: 50,
27 images: ['/images/smartphone-x-1.jpg', '/images/smartphone-x-2.jpg'],
28 categories: {
29 connect: { id: electronicsCategory.id }
30 }
31 },
32 });
33
34 console.log({ electronicsCategory, product1 });
35}
36
37main()
38 .catch((e) => {
39 console.error(e);
40 process.exit(1);
41 })
42 .finally(async () => {
43 await prisma.$disconnect();
44 });Exécution du seeder :
1npx prisma db seedConfiguration dans package.json :
1{
2 "prisma": {
3 "seed": "ts-node prisma/seed.ts"
4 }
5}Le seeding dans Sequelize
1// seeders/YYYYMMDDHHMMSS-demo-user.js
2'use strict';
3
4/** @type {import('sequelize-cli').Migration} */
5module.exports = {
6 async up(queryInterface, Sequelize) {
7 await queryInterface.bulkInsert('Users', [
8 {
9 name: 'John Doe',
10 email: 'john@example.com',
11 createdAt: new Date(),
12 updatedAt: new Date()
13 },
14 {
15 name: 'Jane Smith',
16 email: 'jane@example.com',
17 createdAt: new Date(),
18 updatedAt: new Date()
19 }
20 ]);
21 },
22
23 async down(queryInterface, Sequelize) {
24 await queryInterface.bulkDelete('Users', null, {});
25 }
26};Exécution du seeder :
1npx sequelize-cli db:seed:all
2
3# Annule le dernier seeder
4npx sequelize-cli db:seed:undo
5
6# Annule tous les seeders
7npx sequelize-cli db:seed:undo:allLes transactions dans les migrations
Les transactions garantissent que les migrations sont atomiques - soit tous les changements sont appliqués, soit aucun.
Les transactions dans Prisma
Prisma utilise automatiquement des transactions pour les migrations, sans configuration supplémentaire.
Les transactions dans Sequelize
1// Exemple de migration avec une transaction
2module.exports = {
3 async up(queryInterface, Sequelize) {
4 const transaction = await queryInterface.sequelize.transaction();
5 try {
6 // Première opération
7 await queryInterface.createTable('Products', {
8 id: {
9 allowNull: false,
10 autoIncrement: true,
11 primaryKey: true,
12 type: Sequelize.INTEGER
13 },
14 //
15 }, { transaction });
16
17 // Deuxième opération
18 await queryInterface.addColumn(
19 'Users',
20 'roleId',
21 {
22 type: Sequelize.INTEGER,
23 references: {
24 model: 'Roles',
25 key: 'id'
26 },
27 },
28 { transaction }
29 );
30
31 // Valide la transaction
32 await transaction.commit();
33 } catch (error) {
34 // Annule tous les changements en cas d'erreur
35 await transaction.rollback();
36 throw error;
37 }
38 },
39
40 async down(queryInterface, Sequelize) {
41 //
42 }
43};Migrations avec transformation des données
Parfois, il faut non seulement modifier la structure, mais aussi transformer les données existantes.
1module.exports = {
2 async up(queryInterface, Sequelize) {
3 // 1. Ajoute une nouvelle colonne
4 await queryInterface.addColumn('Users', 'fullName', {
5 type: Sequelize.STRING,
6 allowNull: true
7 });
8
9 // 2. Récupère tous les utilisateurs
10 const users = await queryInterface.sequelize.query(
11 'SELECT id, firstName, lastName FROM "Users"',
12 { type: Sequelize.QueryTypes.SELECT }
13 );
14
15 // 3. Met à jour les données de chaque utilisateur
16 for (const user of users) {
17 await queryInterface.sequelize.query(
18 `UPDATE "Users" SET "fullName" = ? WHERE id = ?`,
19 {
20 replacements: [`${user.firstName} ${user.lastName}`.trim(), user.id],
21 type: Sequelize.QueryTypes.UPDATE
22 }
23 );
24 }
25
26 // 4. Supprime les colonnes inutiles
27 await queryInterface.removeColumn('Users', 'firstName');
28 await queryInterface.removeColumn('Users', 'lastName');
29 },
30
31 async down(queryInterface, Sequelize) {
32 // Logique pour restaurer l'état précédent...
33 }
34};Bonnes pratiques pour les modèles et les migrations
Conception des modèles
- Utilise des noms non ambigus - Les noms de modèles doivent être au singulier (par ex.
User, pasUsers) - Respecte les conventions de nommage - Sois cohérent dans le style (camelCase, snake_case, etc.)
- Documente les modèles - Ajoute des commentaires expliquant le rôle des champs et des relations
- Validation au niveau du modèle - Définis des contraintes et des règles de validation
- Types d'ID - Préfère UUID/CUID aux ID auto-incrémentés pour une meilleure scalabilité
Gérer les migrations
- Migrations petites et ciblées - Une migration doit contenir un seul changement logique
- Teste les migrations - Teste toujours les migrations sur une copie de la base de production avant le déploiement
- Ne modifie jamais les migrations existantes - Crées-en de nouvelles plutôt que de modifier les anciennes
- Fournis toujours des down migrations - Implémente la logique de rollback
- Contrôle de version - Stocke les migrations dans le dépôt de code
Performance
- Index - Ajoute des index sur les colonnes fréquemment recherchées
- Découpe les grosses migrations - Pour les grandes tables, divise les opérations en plus petites parties
- Évite les verrous - Planifie les migrations pour minimiser le temps de verrouillage des tables
- Surveille les performances - Teste le temps d'exécution des migrations sur des jeux de données réalistes
Les migrations dans le contexte de Next.js
Stratégie de migration pour les applications Next.js
Dans les applications Next.js, il est préférable de lancer les migrations :
- Pendant le déploiement - Sur des plateformes comme Vercel, tu peux utiliser des hooks de build/déploiement
- En étape distincte - Dans le pipeline CI/CD, avant le déploiement réel
- Manuellement avant le déploiement - En cas de migrations complexes
Exemple de configuration pour Vercel
Pour les applications hébergées sur Vercel, on peut configurer les migrations dans le fichier package.json :
1{
2 "scripts": {
3 "vercel-build": "prisma migrate deploy && next build"
4 }
5}Déployer les migrations en production
Voici un workflow typique de déploiement des migrations en production :
Développement local :
- Créer et tester les migrations en local
- Committer les migrations dans le contrôle de version
Staging :
- Déployer vers l'environnement de test
- Lancer les tests automatisés
- Vérifier le bon fonctionnement des migrations
Production :
- Créer une sauvegarde de la base de production
- Déployer les migrations pendant les périodes de faible charge
- Surveillance après déploiement
Récapitulatif
Les modèles de données et les migrations forment le socle de la couche de données d'une application Next.js. Une conception réfléchie et une gestion systématique des changements garantissent la stabilité, la scalabilité et la sécurité de l'application.
- Les modèles de données définissent la structure des informations stockées et les relations entre elles
- Les migrations permettent de modifier la structure de la base en toute sécurité
Le choix du bon ORM et de la bonne stratégie de migration dépend des spécificités du projet, mais les principes de conception des modèles restent similaires quelle que soit la technologie.
Dans le prochain module, nous verrons comment récupérer efficacement des données dans les server components de Next.js.
Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Modèles de données et migrations dans Prisma
4// Relations : 1:1, 1:N, N:M
5
6const models = [
7 {
8 name: 'Citizen',
9 fields: [
10 { name: 'id', type: 'String @id @default(cuid())' },
11 { name: 'name', type: 'String' },
12 { name: 'email', type: 'String @unique' },
13 { name: 'district', type: 'District @relation(...)' },
14 { name: 'districtId', type: 'String' },
15 { name: 'energyLogs', type: 'EnergyLog[]' },
16 ],
17 color: '#64ffda',
18 },
19 {
20 name: 'District',
21 fields: [
22 { name: 'id', type: 'String @id @default(cuid())' },
23 { name: 'name', type: 'String @unique' },
24 { name: 'sector', type: 'String' },
25 { name: 'citizens', type: 'Citizen[]' },
26 { name: 'powerGrid', type: 'PowerGrid?' },
27 ],
28 color: '#ff79c6',
29 },
30 {
31 name: 'EnergyLog',
32 fields: [
33 { name: 'id', type: 'String @id @default(cuid())' },
34 { name: 'usage', type: 'Float' },
35 { name: 'citizen', type: 'Citizen @relation(...)' },
36 { name: 'citizenId', type: 'String' },
37 { name: 'createdAt', type: 'DateTime @default(now())' },
38 ],
39 color: '#bd93f9',
40 },
41];
42
43// Exemples de migrations
44const migrations = [
45 { name: '001_init', desc: 'Création des tables Citizen, District' },
46 { name: '002_energy', desc: 'Ajout de la table EnergyLog' },
47 { name: '003_index', desc: 'Index sur email et districtId' },
48];
49
50export default function DataModelsDemo() {
51 const [activeModel, setActiveModel] = useState(0);
52
53 return (
54 <div style={{
55 background: '#0f0f23',
56 minHeight: '100vh',
57 padding: '24px',
58 color: '#e0e0e0',
59 fontFamily: 'monospace'
60 }}>
61 <h1 style={{ color: '#64ffda' }}>
62 Modèles de données et migrations
63 </h1>
64 <p style={{ color: '#8892b0', marginBottom: '20px' }}>
65 Relations : 1 Citizen → 1 District (N:1),
66 1 Citizen → N EnergyLog (1:N)
67 </p>
68
69 {/* Boutons des modèles */}
70 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
71 {models.map((m, i) => (
72 <button
73 key={m.name}
74 onClick={() => setActiveModel(i)}
75 style={{
76 background: activeModel === i ? m.color : '#1a1a2e',
77 color: activeModel === i ? '#0f0f23' : m.color,
78 border: `1px solid ${m.color}`,
79 padding: '8px 16px',
80 borderRadius: '4px',
81 cursor: 'pointer',
82 fontFamily: 'monospace',
83 }}
84 >
85 {m.name}
86 </button>
87 ))}
88 </div>
89
90 {/* Champs du modèle */}
91 <div style={{
92 background: '#1a1a2e',
93 borderRadius: '8px',
94 padding: '16px',
95 border: `1px solid ${models[activeModel].color}`,
96 marginBottom: '20px',
97 }}>
98 <h3 style={{ color: models[activeModel].color }}>
99 model {models[activeModel].name}
100 </h3>
101 {models[activeModel].fields.map((f) => (
102 <div key={f.name} style={{
103 padding: '6px 0',
104 borderBottom: '1px solid #16213e',
105 display: 'flex',
106 gap: '12px'
107 }}>
108 <span style={{ color: '#50fa7b', minWidth: '100px' }}>
109 {f.name}
110 </span>
111 <span style={{ color: '#8892b0' }}>{f.type}</span>
112 </div>
113 ))}
114 </div>
115
116 {/* Migrations */}
117 <h3 style={{ color: '#ff79c6' }}>Historique des migrations</h3>
118 {migrations.map((m, i) => (
119 <div key={m.name} style={{
120 display: 'flex',
121 alignItems: 'center',
122 gap: '12px',
123 padding: '8px',
124 background: '#1a1a2e',
125 borderRadius: '4px',
126 marginBottom: '4px',
127 }}>
128 <span style={{ color: '#50fa7b' }}>✓</span>
129 <code style={{ color: '#bd93f9' }}>{m.name}</code>
130 <span style={{ color: '#8892b0' }}>{m.desc}</span>
131 </div>
132 ))}
133
134 <div style={{
135 marginTop: '16px',
136 padding: '12px',
137 background: '#1a3a2e',
138 borderRadius: '8px',
139 border: '1px solid #2ea043',
140 color: '#7ee787',
141 }}>
142 <strong>Commande :</strong> npx prisma migrate dev --name nom_migration
143 </div>
144 </div>
145 );
146}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. PostgreSQL est :
2. MongoDB stocke les données sous forme de :
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Implémente un tableau de bord avec récupération dynamique de données
- Clique dans l'ordre
Compose la syntaxe SQL de création de table
- Éditeur de code
Crée un panneau de contrôle avec récupération de données en temps réel