Skip to content

Latest commit

 

History

History
1319 lines (990 loc) · 38.8 KB

File metadata and controls

1319 lines (990 loc) · 38.8 KB

🔝 Retour au Sommaire

20.4.2. Database Migrations (Flyway, Liquibase, Alembic)

Introduction

Les migrations de base de données (database migrations) sont un système de gestion des modifications du schéma de votre base de données PostgreSQL au fil du temps. Pensez-y comme un système de contrôle de version (Git) pour votre base de données.

Métaphore : Le carnet de suivi

Imaginez que votre base de données PostgreSQL soit un immeuble en construction :

  • Sans migrations : Vous modifiez l'immeuble au hasard, personne ne sait ce qui a été fait, quand, ni par qui
  • Avec migrations : Chaque modification est documentée dans un carnet (migration file), numérotée, datée, et peut être rejouée ou annulée

Les migrations permettent de tracer l'évolution de votre schéma de base de données, de la synchroniser entre plusieurs environnements (développement, test, production), et de collaborer en équipe sans chaos.

Le problème sans migrations

Scénario typique sans système de migration

Vous développez une application avec PostgreSQL :

Jour 1 - Développeur A :

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

Jour 5 - Développeur B :

-- Ajoute une colonne email
ALTER TABLE users ADD COLUMN email VARCHAR(255);

Jour 10 - Développeur C :

-- Renomme la colonne name en full_name
ALTER TABLE users RENAME COLUMN name TO full_name;

Les problèmes qui surviennent :

1. Désynchronisation des environnements

Base de données locale du Développeur A :
  users(id, name, email)

Base de données locale du Développeur B :
  users(id, full_name, email)

Base de données de production :
  users(id, name)  ← Pas à jour !

→ Le code de l'application ne fonctionne pas partout

2. Impossible de recréer la base

Si vous perdez votre base de données de production, comment la recréer dans le bon état ? Vous avez lancé des dizaines de requêtes SQL au fil des mois, impossible de se souvenir de toutes.

3. Collaboration difficile

Quand plusieurs développeurs modifient le schéma en parallèle, les conflits sont inévitables et difficiles à résoudre.

4. Déploiements risqués

Lors du déploiement d'une nouvelle version de l'application, vous devez vous souvenir manuellement des modifications SQL à appliquer sur la production. Oubliez une modification et l'application plante.

5. Rollback impossible

Si une mise à jour de production échoue, comment revenir en arrière ? Sans historique structuré, c'est très risqué.

La solution : Les migrations de base de données

Principe fondamental

Les migrations transforment les modifications de schéma en fichiers de migration versionnés :

Historique des migrations :
├── 001_create_users_table.sql
├── 002_add_email_to_users.sql
├── 003_rename_name_to_full_name.sql
├── 004_create_orders_table.sql
└── 005_add_index_on_email.sql

Chaque fichier :
  ✅ Est numéroté (ordre d'exécution)
  ✅ Décrit une modification atomique
  ✅ Est versionné dans Git
  ✅ Peut être rejoué sur n'importe quelle base

Le workflow avec migrations

1. Développement local :
   Développeur crée → 006_add_phone_to_users.sql
   Exécute la migration localement
   Commit le fichier dans Git

2. Autres développeurs :
   Pull le code depuis Git
   Exécutent automatiquement la nouvelle migration
   Leurs bases locales sont synchronisées

3. Tests / Staging :
   Déploiement automatique
   Les migrations s'appliquent automatiquement
   Base de test identique au développement

4. Production :
   Déploiement
   Les migrations s'appliquent automatiquement
   Traçabilité complète de ce qui a été appliqué

Métadonnées de migration

Les outils de migration créent une table de suivi dans PostgreSQL :

-- Table créée automatiquement par l'outil de migration
CREATE TABLE schema_migrations (
    version VARCHAR(255) PRIMARY KEY,
    description TEXT,
    applied_at TIMESTAMP DEFAULT NOW(),
    execution_time_ms INTEGER,
    success BOOLEAN
);

Exemple de contenu :

version | description              | applied_at          | success
--------|--------------------------|---------------------|--------
001     | create_users_table       | 2025-01-15 10:00:00 | true
002     | add_email_to_users       | 2025-01-20 14:30:00 | true
003     | rename_name_to_full_name | 2025-01-25 09:15:00 | true
004     | create_orders_table      | 2025-02-01 11:45:00 | true

Grâce à cette table, l'outil sait :

  • Quelles migrations ont été appliquées
  • Quelles migrations restent à appliquer
  • Dans quel ordre les appliquer

Les trois outils principaux

Il existe de nombreux outils de migration, mais trois sont particulièrement populaires dans l'écosystème PostgreSQL :

Tableau comparatif rapide

Critère Flyway Liquibase Alembic
Langage principal Java Java Python
Format migrations SQL (+ Java) SQL, XML, JSON, YAML Python (+ SQL)
Courbe apprentissage Facile Moyenne Moyenne
Écosystème JVM (Java, Kotlin) JVM + autres Python (Django, Flask)
Open Source ✅ Oui (+ version Pro) ✅ Oui (+ version Pro) ✅ Oui
Type de migrations Versionnées, Repeatable Versionnées, ChangeSet Versionnées (Alembic)
Rollback 🔶 Limité (version gratuite) ✅ Complet ✅ Complet
CI/CD ✅ Excellent ✅ Excellent ✅ Excellent
PostgreSQL ✅ Support complet ✅ Support complet ✅ Support complet

1. Flyway : Simplicité et efficacité

Philosophie

Flyway est conçu pour être simple et direct : vous écrivez du SQL pur, vous numérotez vos fichiers, Flyway les exécute dans l'ordre. Pas de format propriétaire, pas de complexité inutile.

Slogan officieux : "Migrations made easy"

Origine

Développé en Java, Flyway est particulièrement populaire dans l'écosystème Java/Spring Boot, mais fonctionne avec n'importe quelle stack via sa CLI (Command Line Interface).

Fonctionnement

Structure des fichiers de migration

Flyway utilise une convention de nommage stricte :

Format : V{version}__{description}.sql

Exemples :
  V1__create_users_table.sql
  V2__add_email_to_users.sql
  V3__create_orders_table.sql
  V4__add_index_on_email.sql
  V2.1__add_phone_to_users.sql  (version intermédiaire)

Règles :

  • Commence par V (Versioned migration)
  • Suivi d'un numéro de version (1, 2, 3, ou 1.1, 1.2, etc.)
  • Deux underscores __
  • Description lisible (snake_case)
  • Extension .sql

Exemple de migration Flyway

Fichier : V1__create_users_table.sql

-- Flyway migration: Create users table
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    full_name VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Créer un index pour optimiser les recherches
CREATE INDEX idx_users_email ON users(email);

Fichier : V2__add_phone_to_users.sql

-- Flyway migration: Add phone number to users
ALTER TABLE users  
ADD COLUMN phone VARCHAR(20);  

-- Commentaire pour documentation
COMMENT ON COLUMN users.phone IS 'User phone number (optional)';

Commandes Flyway principales

flyway migrate
  → Applique toutes les migrations en attente
  → Exécute les fichiers V1, V2, V3... dans l'ordre
  → Met à jour la table flyway_schema_history

flyway info
  → Affiche l'état de toutes les migrations
  → Lesquelles sont appliquées, lesquelles sont en attente

flyway validate
  → Vérifie que les migrations appliquées n'ont pas été modifiées
  → Détecte les incohérences

flyway clean
  → ⚠️ Supprime TOUTES les tables (base vide)
  → Uniquement pour développement !

flyway repair
  → Corrige la table flyway_schema_history
  → Utile en cas de migration échouée

Table de métadonnées

Flyway crée automatiquement la table flyway_schema_history :

CREATE TABLE flyway_schema_history (
    installed_rank INT NOT NULL,
    version VARCHAR(50),
    description VARCHAR(200) NOT NULL,
    type VARCHAR(20) NOT NULL,
    script VARCHAR(1000) NOT NULL,
    checksum INT,
    installed_by VARCHAR(100) NOT NULL,
    installed_on TIMESTAMP NOT NULL DEFAULT NOW(),
    execution_time INT NOT NULL,
    success BOOLEAN NOT NULL
);

Cette table enregistre chaque migration exécutée avec son checksum (empreinte) pour détecter toute modification ultérieure.

Migrations répétables (Repeatable Migrations)

Flyway propose aussi des migrations répétables qui s'exécutent à chaque fois qu'elles changent :

Format : R__{description}.sql

Exemples :
  R__create_view_active_users.sql
  R__create_function_calculate_total.sql

Cas d'usage :
  - Vues (VIEW)
  - Fonctions (FUNCTION)
  - Procédures stockées
  - Triggers

→ Ré-exécutées automatiquement si le fichier change

Exemple : R__create_view_active_users.sql

-- Repeatable migration: Vue des utilisateurs actifs
CREATE OR REPLACE VIEW active_users AS  
SELECT id, email, full_name  
FROM users  
WHERE deleted_at IS NULL  
ORDER BY created_at DESC;  

Avantages de Flyway

  • Simplicité : SQL pur, pas de syntaxe propriétaire à apprendre
  • Convention de nommage claire : On comprend immédiatement l'ordre et le contenu
  • Rapide à mettre en place : Configuration minimale
  • CLI puissante : Utilisable dans n'importe quel projet (Java, Node, Python, Go...)
  • Intégration CI/CD : Facile à intégrer dans les pipelines
  • Validation automatique : Détecte les modifications de migrations appliquées

Inconvénients de Flyway

  • Rollback limité : Version gratuite ne supporte pas les rollbacks automatiques (version Pro payante requise)
  • Pas de génération automatique : Vous devez écrire le SQL manuellement
  • Migrations bidirectionnelles difficiles : Pas de concept "UP/DOWN" natif
  • Version Pro payante : Fonctionnalités avancées (rollback, undo, dry-run) nécessitent une licence

Cas d'usage idéaux

  • Applications Java/Spring Boot (intégration native)
  • Projets préférant SQL pur sans abstraction
  • Équipes voulant un outil simple et direct
  • CI/CD automatisés nécessitant peu de configuration

2. Liquibase : Flexibilité et puissance

Philosophie

Liquibase se veut flexible et agnostique : vous pouvez écrire vos migrations en SQL, XML, JSON ou YAML. Il offre aussi des capacités de rollback avancées et de génération automatique de migrations.

Slogan officieux : "Source control for your database"

Origine

Développé en Java comme Flyway, mais avec une approche plus abstraite et orientée métadonnées.

Fonctionnement

Structure des fichiers de migration

Liquibase utilise des ChangeSets (ensembles de modifications) organisés dans un fichier principal :

Structure typique :
├── db/
│   ├── changelog/
│   │   ├── db.changelog-master.xml (ou .yaml/.json)
│   │   ├── changes/
│   │   │   ├── 001-create-users-table.sql
│   │   │   ├── 002-add-email-to-users.sql
│   │   │   └── 003-create-orders-table.sql

Formats supportés

1. XML (Format historique)

Fichier : db.changelog-master.xml

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
    http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">

    <changeSet id="1" author="john">
        <createTable tableName="users">
            <column name="id" type="SERIAL">
                <constraints primaryKey="true"/>
            </column>
            <column name="email" type="VARCHAR(255)">
                <constraints nullable="false" unique="true"/>
            </column>
            <column name="full_name" type="VARCHAR(255)">
                <constraints nullable="false"/>
            </column>
        </createTable>
    </changeSet>

    <changeSet id="2" author="jane">
        <addColumn tableName="users">
            <column name="phone" type="VARCHAR(20)"/>
        </addColumn>
    </changeSet>

</databaseChangeLog>
2. YAML (Format moderne et lisible)

Fichier : db.changelog-master.yaml

databaseChangeLog:
  - changeSet:
      id: 1
      author: john
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: SERIAL
                  constraints:
                    primaryKey: true
              - column:
                  name: email
                  type: VARCHAR(255)
                  constraints:
                    nullable: false
                    unique: true
              - column:
                  name: full_name
                  type: VARCHAR(255)
                  constraints:
                    nullable: false

  - changeSet:
      id: 2
      author: jane
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: phone
                  type: VARCHAR(20)
3. JSON

Fichier : db.changelog-master.json

{
  "databaseChangeLog": [
    {
      "changeSet": {
        "id": "1",
        "author": "john",
        "changes": [
          {
            "createTable": {
              "tableName": "users",
              "columns": [
                {
                  "column": {
                    "name": "id",
                    "type": "SERIAL",
                    "constraints": {
                      "primaryKey": true
                    }
                  }
                }
              ]
            }
          }
        ]
      }
    }
  ]
}
4. SQL (comme Flyway)

Liquibase peut aussi utiliser du SQL pur avec des annotations spéciales :

Fichier : 001-create-users-table.sql

--liquibase formatted sql

--changeset john:1
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    full_name VARCHAR(255) NOT NULL
);
--rollback DROP TABLE users;

--changeset jane:2
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
--rollback ALTER TABLE users DROP COLUMN phone;

Note importante : La directive --rollback permet de définir comment annuler la migration !

Concept de ChangeSet

Un ChangeSet est une unité atomique de modification :

ChangeSet :
  - id: Identifiant unique (ex: "1", "create-users", "2024-01-15-001")
  - author: Qui a créé ce changement
  - changes: Liste des modifications SQL/DDL
  - rollback: (optionnel) Comment annuler ce changement
  - preconditions: (optionnel) Conditions pour exécuter
  - context: (optionnel) Environnements cibles (dev, prod, etc.)

Commandes Liquibase principales

liquibase update
  → Applique tous les changeSets en attente
  → Équivalent de flyway migrate

liquibase rollback <tag>
  → Annule les changeSets jusqu'à un tag donné
  → ⭐ Fonctionnalité clé de Liquibase

liquibase rollbackCount <number>
  → Annule les N derniers changeSets

liquibase status
  → Affiche les changeSets en attente
  → Équivalent de flyway info

liquibase validate
  → Vérifie la cohérence des changeSets

liquibase generateChangeLog
  → 🚀 Génère automatiquement un changelog depuis une base existante
  → Très utile pour adopter Liquibase sur un projet existant

liquibase diff
  → Compare deux bases et génère les différences
  → Utile pour synchroniser dev ↔ prod

liquibase tag <tagName>
  → Crée un point de restauration nommé

Table de métadonnées

Liquibase crée deux tables :

1. databasechangelog (historique des changeSets appliqués)

CREATE TABLE databasechangelog (
    id VARCHAR(255) NOT NULL,
    author VARCHAR(255) NOT NULL,
    filename VARCHAR(255) NOT NULL,
    dateexecuted TIMESTAMP NOT NULL,
    orderexecuted INT NOT NULL,
    exectype VARCHAR(10) NOT NULL,
    md5sum VARCHAR(35),
    description VARCHAR(255),
    comments VARCHAR(255),
    tag VARCHAR(255),
    liquibase VARCHAR(20),
    contexts VARCHAR(255),
    labels VARCHAR(255),
    deployment_id VARCHAR(10)
);

2. databasechangeloglock (verrou pour éviter les exécutions concurrentes)

CREATE TABLE databasechangeloglock (
    id INT NOT NULL PRIMARY KEY,
    locked BOOLEAN NOT NULL,
    lockgranted TIMESTAMP,
    lockedby VARCHAR(255)
);

Avantages de Liquibase

  • Rollback intégré : Annuler des migrations facilement (même en version gratuite)
  • Multi-format : SQL, XML, YAML, JSON au choix
  • Génération automatique : generateChangeLog pour bases existantes
  • Diff entre bases : Détecter automatiquement les différences
  • Préconditions : Exécuter un changeSet seulement si une condition est remplie
  • Contextes : Appliquer certains changeSets seulement en dev, prod, etc.
  • Agnostique : Support étendu de nombreuses bases de données

Inconvénients de Liquibase

  • Courbe d'apprentissage : Plus complexe que Flyway (syntaxe XML/YAML à apprendre)
  • Verbosité : Les fichiers XML/YAML peuvent être longs
  • Configuration : Plus de paramètres à configurer
  • Abstraction : L'abstraction XML/YAML peut éloigner du SQL réel
  • Debugging : Erreurs parfois difficiles à comprendre (surtout en XML)

Cas d'usage idéaux

  • Projets nécessitant des rollbacks fréquents
  • Migrations de bases de données existantes (generateChangeLog)
  • Déploiements multi-environnements complexes (dev, test, staging, prod)
  • Équipes préférant une abstraction au-dessus du SQL
  • Besoins de synchronisation entre bases (diff)

3. Alembic : Le choix Python

Philosophie

Alembic est l'outil de migration standard de l'écosystème Python, particulièrement utilisé avec SQLAlchemy (l'ORM Python le plus populaire). Il combine flexibilité du Python avec SQL pur.

Slogan officieux : "A database migration tool for SQLAlchemy"

Origine

Développé par le créateur de SQLAlchemy (Mike Bayer), Alembic est le choix naturel pour les applications Python (Django, Flask, FastAPI).

Note : Django a son propre système de migrations intégré, mais Alembic peut être utilisé pour des besoins avancés.

Fonctionnement

Structure des fichiers de migration

Structure typique :  
project/  
├── alembic/
│   ├── versions/
│   │   ├── 001_create_users_table.py
│   │   ├── 002_add_email_to_users.py
│   │   └── 003_create_orders_table.py
│   ├── env.py (configuration Alembic)
│   └── script.py.mako (template de migration)
├── alembic.ini (configuration principale)
└── models.py (vos modèles SQLAlchemy)

Format des migrations

Alembic utilise des fichiers Python avec deux fonctions : upgrade() et downgrade().

Fichier : 001_create_users_table.py

"""Create users table

Revision ID: 001  
Revises:  
Create Date: 2025-01-15 10:00:00  
"""
from alembic import op  
import sqlalchemy as sa  

# Identifiants de révision
revision = '001'  
down_revision = None  
branch_labels = None  
depends_on = None  

def upgrade():
    """Applique la migration (UP)"""
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('email', sa.String(255), unique=True, nullable=False),
        sa.Column('full_name', sa.String(255), nullable=False),
        sa.Column('created_at', sa.TIMESTAMP(timezone=True),
                  server_default=sa.text('NOW()'))
    )

    # Créer un index
    op.create_index('idx_users_email', 'users', ['email'])

def downgrade():
    """Annule la migration (DOWN)"""
    op.drop_index('idx_users_email', 'users')
    op.drop_table('users')

Fichier : 002_add_phone_to_users.py

"""Add phone to users

Revision ID: 002  
Revises: 001  
Create Date: 2025-01-20 14:30:00  
"""
from alembic import op  
import sqlalchemy as sa  

revision = '002'  
down_revision = '001'  # Dépend de la migration 001  

def upgrade():
    """Ajoute la colonne phone"""
    op.add_column('users',
                  sa.Column('phone', sa.String(20), nullable=True))

def downgrade():
    """Supprime la colonne phone"""
    op.drop_column('users', 'phone')

SQL brut dans Alembic

Vous pouvez aussi écrire du SQL pur avec op.execute() :

def upgrade():
    """Migration avec SQL brut"""
    op.execute("""
        CREATE TABLE orders (
            id SERIAL PRIMARY KEY,
            user_id INTEGER REFERENCES users(id),
            total NUMERIC(10, 2) NOT NULL,
            created_at TIMESTAMPTZ DEFAULT NOW()
        )
    """)

    # Index avec SQL pur
    op.execute("""
        CREATE INDEX idx_orders_user_id ON orders(user_id)
    """)

def downgrade():
    """Rollback avec SQL brut"""
    op.execute("DROP TABLE orders")

Commandes Alembic principales

alembic init alembic
  → Initialise Alembic dans le projet
  → Crée la structure de dossiers

alembic revision -m "Create users table"
  → Crée un nouveau fichier de migration vide
  → Vous devez écrire upgrade() et downgrade()

alembic revision --autogenerate -m "Add phone column"
  → 🚀 Génère automatiquement la migration en comparant les modèles SQLAlchemy
  → Très puissant pour éviter d'écrire le SQL manuellement

alembic upgrade head
  → Applique toutes les migrations en attente
  → Équivalent de flyway migrate / liquibase update

alembic downgrade -1
  → Annule la dernière migration
  → Exécute la fonction downgrade()

alembic downgrade <revision>
  → Annule jusqu'à une révision donnée

alembic current
  → Affiche la révision actuelle de la base

alembic history
  → Affiche l'historique de toutes les migrations

alembic show <revision>
  → Affiche les détails d'une migration spécifique

alembic stamp head
  → Marque la base comme étant à jour sans exécuter les migrations
  → Utile lors de l'adoption d'Alembic sur une base existante

Génération automatique (Autogenerate)

La killer feature d'Alembic : comparer vos modèles SQLAlchemy avec la base actuelle et générer automatiquement les migrations.

Exemple de workflow :

1. Vous modifiez votre modèle SQLAlchemy :

# models.py
class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    email = Column(String(255), unique=True, nullable=False)
    full_name = Column(String(255), nullable=False)
    phone = Column(String(20))  # ← Nouvelle colonne ajoutée
    created_at = Column(TIMESTAMP(timezone=True), server_default=func.now())

2. Vous lancez la génération automatique :

alembic revision --autogenerate -m "Add phone to users"

3. Alembic génère automatiquement :

def upgrade():
    op.add_column('users', sa.Column('phone', sa.String(20), nullable=True))

def downgrade():
    op.drop_column('users', 'phone')

4. Vous vérifiez et appliquez :

alembic upgrade head

⚠️ Important : L'autogenerate n'est pas parfait. Vous devez toujours vérifier la migration générée. Il ne détecte pas tout (changements de données, renommages complexes, etc.).

Table de métadonnées

Alembic crée la table alembic_version :

CREATE TABLE alembic_version (
    version_num VARCHAR(32) NOT NULL PRIMARY KEY
);

Contrairement à Flyway/Liquibase, cette table ne stocke que la version actuelle, pas l'historique complet. L'historique est dans les fichiers de migration.

Avantages d'Alembic

  • Intégration SQLAlchemy : Génération automatique depuis les modèles ORM
  • Rollback natif : Fonctions upgrade()/downgrade() obligatoires
  • Flexibilité Python : Logique complexe possible dans les migrations (loops, conditions, etc.)
  • SQL brut supporté : Pas obligé d'utiliser l'abstraction si vous préférez SQL pur
  • Branching : Support de branches de migrations parallèles (avancé)
  • Écosystème Python : Intégration naturelle avec Flask, FastAPI, Django (via extension)

Inconvénients d'Alembic

  • Python uniquement : Pas utilisable dans des projets non-Python
  • Courbe d'apprentissage : API Alembic + SQLAlchemy à apprendre
  • Autogenerate imparfait : Ne détecte pas tout, nécessite vérification manuelle
  • Moins populaire hors Python : Flyway et Liquibase ont des communautés plus larges
  • Configuration initiale : Plus complexe que Flyway

Cas d'usage idéaux

  • Applications Python (Flask, FastAPI, Django)
  • Projets utilisant SQLAlchemy comme ORM
  • Besoin de génération automatique de migrations
  • Migrations nécessitant de la logique Python complexe
  • Équipes Python confortables avec l'écosystème

Concepts communs aux trois outils

1. Migration versionnée (Up)

Toutes les migrations ont une direction "vers l'avant" (upgrade, migrate, up) qui applique les modifications.

État initial :
  Table users (id, name)

Migration 001 (UP) :
  ALTER TABLE users ADD COLUMN email VARCHAR(255)

État final :
  Table users (id, name, email)

2. Rollback (Down)

La capacité d'annuler une migration pour revenir à l'état précédent.

État actuel :
  Table users (id, name, email)

Migration 001 (DOWN) :
  ALTER TABLE users DROP COLUMN email

État final :
  Table users (id, name)

Comparaison :

  • Flyway (gratuit) : ❌ Pas de rollback automatique (version Pro payante)
  • Liquibase : ✅ Rollback complet (même version gratuite)
  • Alembic : ✅ Rollback via downgrade()

3. Idempotence

Une migration doit être idempotente : si elle est exécutée plusieurs fois, elle ne doit pas causer d'erreur.

Exemple non-idempotent :

-- ❌ Erreur si la table existe déjà
CREATE TABLE users (id SERIAL PRIMARY KEY);

Exemple idempotent :

-- ✅ Ne fait rien si la table existe
CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY);

4. Transactions

Les migrations doivent s'exécuter dans une transaction PostgreSQL pour garantir l'atomicité.

BEGIN;
  -- Migration 001
  CREATE TABLE users (...);
  CREATE INDEX idx_users_email ON users(email);
  -- Si une erreur survient ici, tout est annulé
COMMIT;

Si une partie de la migration échoue, PostgreSQL annule tout (ROLLBACK automatique).

5. Checksum / Hash

Les outils calculent une empreinte (hash) de chaque migration pour détecter toute modification après application.

Migration 001_create_users.sql appliquée :
  Checksum: a3f5b2c...

Quelqu'un modifie 001_create_users.sql :
  Nouveau checksum: e7d9a1f...

Outil détecte : ⚠️ ERREUR - Migration modifiée après application !

Règle d'or : Ne JAMAIS modifier une migration déjà appliquée en production. Créez une nouvelle migration.

6. Baseline / Init

Pour adopter un outil de migration sur une base existante, il faut définir un point de départ (baseline).

Base de données existante avec 20 tables :

Option 1 - Baseline :
  alembic stamp head
  → Marque la base comme étant à la version actuelle
  → Les futures migrations s'appliquent à partir de là

Option 2 - Générer le changelog initial :
  liquibase generateChangeLog
  → Crée une migration représentant l'état actuel
  → Cette migration devient la v1

Stratégies et bonnes pratiques

1. Convention de nommage

Utilisez des noms de migration descriptifs et datés :

✅ Bon :
  V001__create_users_table.sql
  V002__add_email_index_to_users.sql
  2025_01_15_001_create_users_table.py

❌ Mauvais :
  migration1.sql
  fix.sql
  update.sql

2. Une migration = une modification atomique

Chaque migration doit faire une seule chose logique :

✅ Bon :
  001_create_users_table.sql
  002_create_orders_table.sql
  003_add_foreign_key_orders_to_users.sql

❌ Mauvais :
  001_create_all_tables_and_indexes_and_data.sql
  → Difficile à déboguer si ça échoue

3. Toujours tester les rollbacks

Si vous implémentez un rollback (Liquibase, Alembic), testez-le avant de déployer :

# Appliquer la migration
alembic upgrade +1

# Tester le rollback
alembic downgrade -1

# Ré-appliquer
alembic upgrade +1

4. Migrations de données vs migrations de schéma

Séparez les migrations qui modifient le schéma de celles qui modifient les données :

Migration de schéma :

-- 010_add_status_to_orders.sql
ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending';

Migration de données :

-- 011_populate_order_status.sql
UPDATE orders SET status = 'completed' WHERE completed_at IS NOT NULL;  
UPDATE orders SET status = 'cancelled' WHERE cancelled_at IS NOT NULL;  

Pourquoi séparer ? Les migrations de données peuvent être lentes sur de grandes tables et nécessitent une stratégie différente (batch processing, zero-downtime, etc.).

5. Migrations réversibles avec précaution

Certaines migrations sont destructives et ne peuvent pas être annulées sans perte de données :

-- ⚠️ Migration destructive
ALTER TABLE users DROP COLUMN phone;
-- Rollback : Impossible de récupérer les données supprimées !

Stratégie safe :

Migration 1 : Ajouter nouvelle colonne phone_number  
Migration 2 : Copier les données de phone vers phone_number  
Migration 3 : Déployer le code utilisant phone_number  
Migration 4 : (Plus tard) Supprimer l'ancienne colonne phone  

6. Versionner les migrations dans Git

Les fichiers de migration doivent être versionnés dans votre dépôt Git :

Git repository :
├── src/
├── tests/
├── migrations/  ← Versionné dans Git
│   ├── V001__create_users.sql
│   ├── V002__add_email_index.sql
└── README.md

→ Toute l'équipe partage les mêmes migrations
→ Historique complet dans Git

7. Ne jamais modifier une migration appliquée

Règle absolue : Une fois qu'une migration est appliquée en production, elle est immuable.

❌ Jamais ça :
  Modifier V001__create_users.sql après l'avoir déployée

✅ Faire ça :
  Créer V005__fix_users_table.sql pour corriger

8. Environnements multiples

Votre stratégie de migration doit fonctionner sur tous les environnements :

Développement local :
  → Migrations appliquées au fur et à mesure
  → Base peut être détruite/recréée facilement

Staging / Test :
  → Copie de production (anonymisée)
  → Migrations testées avant production

Production :
  → Migrations appliquées avec prudence
  → Backup avant migration
  → Plan de rollback prêt

9. Automatisation CI/CD

Intégrez les migrations dans votre pipeline de déploiement :

Pipeline CI/CD :
1. Tests unitaires
2. Build de l'application
3. ✅ Vérifier les migrations (validate)
4. Déployer l'application
5. ✅ Appliquer les migrations (migrate)
6. Tests d'intégration
7. Déploiement en production

10. Documentation des migrations

Ajoutez des commentaires dans vos migrations pour expliquer pourquoi :

-- Migration: Add customer_tier column for loyalty program
-- Context: Marketing team requested tiering system for customers
-- Ticket: JIRA-1234
-- Author: John Doe
-- Date: 2025-01-15

ALTER TABLE customers  
ADD COLUMN customer_tier VARCHAR(20) DEFAULT 'bronze'  
CHECK (customer_tier IN ('bronze', 'silver', 'gold', 'platinum'));  

COMMENT ON COLUMN customers.customer_tier IS
'Customer loyalty tier: bronze (default), silver, gold, platinum.
Based on total purchase amount in last 12 months.';

Scénarios d'utilisation avancés

Scénario 1 : Migration avec Zero-Downtime

Problème : Vous devez renommer une colonne sans arrêter l'application.

Stratégie en plusieurs étapes :

Étape 1 (Migration 010) : Ajouter nouvelle colonne
  ALTER TABLE users ADD COLUMN full_name VARCHAR(255);

Étape 2 (Migration 011) : Copier les données
  UPDATE users SET full_name = name WHERE full_name IS NULL;

Étape 3 (Déploiement code v2) :
  Application lit/écrit dans les deux colonnes (name et full_name)

Étape 4 (Migration 012) : Rendre full_name NOT NULL
  ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;

Étape 5 (Déploiement code v3) :
  Application utilise uniquement full_name

Étape 6 (Migration 013 - Plus tard) : Supprimer ancienne colonne
  ALTER TABLE users DROP COLUMN name;

Avantage : Aucune interruption de service.

Scénario 2 : Migration sur table volumineuse

Problème : Vous devez ajouter une colonne NOT NULL à une table de 100 millions de lignes.

Mauvaise approche :

-- ❌ Bloque la table pendant des heures
ALTER TABLE orders ADD COLUMN processed BOOLEAN NOT NULL DEFAULT false;

Bonne approche :

-- Migration 020: Ajouter colonne nullable
ALTER TABLE orders ADD COLUMN processed BOOLEAN;

-- Migration 021: Remplir par batch (en plusieurs petits UPDATE)
-- Peut être fait via un script Python/Job séparé
UPDATE orders SET processed = false  
WHERE id BETWEEN 1 AND 1000000 AND processed IS NULL;  

-- Répéter pour tous les ranges...

-- Migration 022: Rendre NOT NULL une fois rempli
ALTER TABLE orders ALTER COLUMN processed SET DEFAULT false;  
ALTER TABLE orders ALTER COLUMN processed SET NOT NULL;  

Scénario 3 : Branches parallèles de développement

Problème : Deux développeurs créent des migrations en parallèle.

Développeur A crée :

V010__add_phone_to_users.sql

Développeur B crée (en même temps) :

V010__add_address_to_users.sql  ← Conflit !

Solution avec Liquibase :

# Utiliser des IDs uniques (timestamp + description)
changeSet:
  id: 2025-01-15-10h30-add-phone
  author: dev-a

changeSet:
  id: 2025-01-15-11h00-add-address
  author: dev-b

Solution avec Alembic :

# Alembic génère des IDs uniques automatiquement
alembic revision -m "Add phone"
# → Génère : 0a3f2b1c_add_phone.py

alembic revision -m "Add address"
# → Génère : 7d9e4f5a_add_address.py

# Fusion des branches :
alembic merge heads -m "Merge phone and address"

Scénario 4 : Rollback partiel

Problème : Vous voulez annuler seulement certaines migrations, pas toutes.

Avec Liquibase (tags) :

# Créer un tag avant déploiement risqué
liquibase tag "before-major-refactor"

# Déployer plusieurs migrations
liquibase update

# Si problème, revenir au tag
liquibase rollback "before-major-refactor"

Avec Alembic :

# Descendre à une révision spécifique
alembic downgrade a3f2b1c

# Ou annuler les 3 dernières migrations
alembic downgrade -3

Comparaison finale et choix

Quand choisir Flyway ?

  • ✅ Vous voulez la simplicité avant tout
  • ✅ Vous écrivez du SQL pur et ne voulez pas d'abstraction
  • ✅ Vous êtes dans l'écosystème Java/Spring Boot
  • ✅ Vous n'avez pas besoin de rollbacks fréquents
  • ✅ Vous voulez une intégration CI/CD facile
  • ✅ Vous avez un budget limité (version gratuite suffit)

Quand choisir Liquibase ?

  • ✅ Vous avez besoin de rollbacks réguliers
  • ✅ Vous voulez générer automatiquement des migrations depuis une base existante
  • ✅ Vous gérez des environnements multiples complexes (dev/test/staging/prod)
  • ✅ Vous voulez comparer et synchroniser plusieurs bases (diff)
  • ✅ Vous préférez XML/YAML au SQL pur
  • ✅ Vous avez besoin de préconditions et contextes avancés

Quand choisir Alembic ?

  • ✅ Vous développez en Python (Flask, FastAPI, SQLAlchemy)
  • ✅ Vous utilisez un ORM et voulez générer les migrations automatiquement
  • ✅ Vous voulez écrire de la logique Python dans les migrations
  • ✅ Vous avez besoin de rollbacks avec une approche up/down claire
  • ✅ Votre équipe est confortable avec Python
  • ✅ Vous voulez du SQL brut avec la flexibilité Python

Tableau décisionnel

Critère Flyway Liquibase Alembic
Simplicité ⭐⭐⭐ ⭐⭐ ⭐⭐
Rollback automatique ❌ (Pro)
Génération automatique
SQL pur
Multi-langage ❌ (Python)
Courbe d'apprentissage Facile Moyenne Moyenne
Communauté Large Large Python
Open Source gratuit complet 🔶

Conclusion

Les migrations de base de données sont essentielles pour maintenir une base PostgreSQL évolutive, traçable et synchronisée entre environnements. Les trois outils présentés (Flyway, Liquibase, Alembic) offrent des approches différentes mais convergent vers le même objectif : gérer le schéma comme du code.

Points clés à retenir :

  1. Les migrations versionnent votre schéma comme Git versionne votre code
  2. Chaque outil a ses forces : simplicité (Flyway), flexibilité (Liquibase), intégration Python (Alembic)
  3. Les bonnes pratiques sont universelles : atomicité, idempotence, tests, documentation
  4. Ne jamais modifier une migration appliquée : créez une nouvelle migration
  5. Automatisez dans votre CI/CD pour éviter les erreurs humaines
  6. Testez les rollbacks même si vous ne les utilisez jamais en production

Prochaines étapes

Après avoir maîtrisé les migrations, explorez :

  • 20.4.3. Schema versioning : Stratégies de versionnement avancées
  • 20.4.4. Zero-downtime deployments : Déployer sans interruption
  • 19.3. Migrations majeures : Migrer de PostgreSQL 17 vers 18

Les migrations sont la fondation d'un projet PostgreSQL professionnel. Investir du temps dans leur mise en place vous épargnera d'innombrables heures de debugging et de conflits en production.


Ressources complémentaires :

⏭️ Schema versioning