Image de couverture : Migrations de base de données sans downtime : passer de l'MVP à l'Enterprise Grade
Bases de données

Migrations de base de données sans downtime : passer de l'MVP à l'Enterprise Grade

29 June 2026
6 min de lecture
4 vues
Sébastien Muler

Migrations de base de données sans downtime : passer de l'MVP à l'Enterprise Grade

Déployer une application web en production est une chose. La faire évoluer sans interrompre le service pour des milliers d'utilisateurs en est une autre. C'est précisément le défi que pose la gestion des migrations de base de données à grande échelle — un sujet abordé de façon très concrète dans cet article de Prajapati Paresh sur dev.to, et que nous allons transposer ici dans une perspective PHP/Symfony.

Le piège classique de la mise à l'échelle

Sur un projet MVP, le workflow de déploiement est simple et rassurant : on active la maintenance, on lance les migrations, on déploie le nouveau code, on désactive la maintenance. Deux minutes d'indisponibilité, pas de problème.

Mais dès que votre SaaS atteint quelques milliers d'utilisateurs actifs — et a fortiori des clients enterprise avec des SLA exigeants — ce « downtime de deux minutes » devient inacceptable, voire contractuellement interdit.

Le vrai danger surgit lorsqu'on tente de renommer ou supprimer une colonne pendant que l'application tourne. Imaginons qu'on renomme first_name en full_name : il existe une fraction de seconde où la base de données connaît déjà la nouvelle colonne, mais où les serveurs d'application exécutent encore l'ancien code. Durant cette fenêtre, toute requête d'insertion déclenche une erreur SQL fatale. La cause est structurelle, pas accidentelle.

La solution : le pattern Expand and Contract

L'approche reconnue pour résoudre ce problème s'appelle le pattern Expand and Contract (ou expand/migrate/contract). Le principe : ne jamais modifier de façon destructive une structure de données en une seule opération. On décompose le changement en plusieurs phases de déploiement indépendantes et sûres.

Phase 1 — Expand : on ajoute sans supprimer

On crée une migration qui ajoute la nouvelle colonne (full_name) sans toucher à l'ancienne (first_name). En parallèle, on met à jour le modèle applicatif pour écrire dans les deux colonnes simultanément, et pour lire depuis la nouvelle colonne en priorité si elle est renseignée.

// Symfony : exemple avec une migration Doctrine
public function up(Schema $schema): void
{
    $table = $schema->getTable('users');
    $table->addColumn('full_name', 'string', ['length' => 255, 'notnull' => false]);
}

Côté entité ou repository, la logique de double-écriture peut être encapsulée dans un setter ou un subscriber Doctrine :

public function setFullName(string $name): void
{
    $this->fullName = $name;
    $this->firstName = $name; // rétrocompatibilité
}

À l'issue de cette phase, on déploie. L'application continue de fonctionner sans interruption : l'ancienne colonne est toujours là, la nouvelle se remplit progressivement.

Phase 2 — Migrate : on backfille les données historiques

Une fois la Phase 1 en production, on lance une migration de données pour remplir full_name à partir de first_name pour tous les enregistrements existants. Cette opération peut être longue sur de gros volumes ; elle doit être exécutée par lots (batch) pour ne pas verrouiller la table.

-- Exécution par batch pour éviter les locks
UPDATE users
SET full_name = first_name
WHERE full_name IS NULL
LIMIT 1000;

En Symfony, cette étape peut être orchestrée via une commande Symfony dédiée, planifiée ou déclenchée manuellement, avec une progression loggée.

Phase 3 — Contract : on supprime l'ancienne colonne

Seulement après avoir vérifié que :

  • 100 % des lignes ont une valeur full_name non nulle,
  • le code ne référence plus nulle part first_name,
  • la phase est restée stable en production pendant plusieurs jours...

...on crée une nouvelle migration qui supprime l'ancienne colonne.

public function up(Schema $schema): void
{
    $table = $schema->getTable('users');
    $table->dropColumn('first_name');
}

Cette migration est anodine : la colonne supprimée n'est plus utilisée par aucun code en production.

Aller plus loin : les verrous et les indexes

Le pattern Expand/Contract couvre les renommages et suppressions de colonnes. Mais d'autres opérations courantes posent des problèmes similaires :

  • Ajout d'une contrainte NOT NULL : il faut d'abord backfiller les valeurs nulles existantes, puis ajouter la contrainte. Inverser l'ordre provoque une erreur immédiate.
  • Création d'un index sur une grande table : en PostgreSQL, privilégier CREATE INDEX CONCURRENTLY pour éviter de poser un verrou exclusif sur la table pendant la construction de l'index.
  • Changement de type de colonne : opération particulièrement risquée, à décomposer en ajout d'une nouvelle colonne + backfill + suppression de l'ancienne.

Ces règles s'appliquent quelle que soit la stack — Laravel ou Symfony — et quel que soit le SGBD, même si PostgreSQL offre des primitives particulièrement adaptées (CONCURRENTLY, transactions DDL, etc.).

Ce que ça change dans votre workflow CI/CD

Adopter ce pattern implique un changement de culture autant que de technique :

  • Les migrations et le code ne se déploient plus ensemble : une migration de Phase 1 doit être en production avant le code qui l'utilise.
  • Les revues de code intègrent la dimension migration : on vérifie systématiquement si une migration est rétrocompatible avec le code actuellement en production.
  • Les pipelines CI/CD intègrent des vérifications : des outils comme doctrine-migrations ou des linters de migrations peuvent détecter les opérations destructives non sécurisées.

Cette discipline ralentit légèrement le rythme de déploiement des changements de schéma, mais elle élimine une catégorie entière d'incidents de production.

Conclusion

Le passage de l'MVP à l'enterprise grade ne se joue pas uniquement sur les performances ou la scalabilité horizontale. Il se joue aussi sur la maîtrise des opérations de maintenance, et notamment sur la capacité à faire évoluer le schéma de données sans jamais interrompre le service.

Le pattern Expand and Contract est une réponse éprouvée à ce défi. Il demande de la rigueur et une coordination entre les équipes dev et ops, mais il offre en retour une fiabilité de déploiement incomparable. Que vous soyez sur Laravel, Symfony, ou toute autre stack PHP, les principes restent les mêmes.

📖 Source originale : Zero-Downtime Database Migrations in Laravel par Prajapati Paresh sur dev.to.

Partager cet article