Passer au contenu
DedicatedPHP Contact

Migrations de base de données sans interruption en PHP

Découvrez comment faire évoluer des schémas PHP avec compatibilité temporaire, migrations reprenables, validation des données et rollback opérationnel.

Schéma éditorial des phases étendre, migrer et retirer pour modifier un schéma de base de données dans une application PHP

Une migration de schéma peut échouer même si la modification du code a passé les tests. En production, une application ne change généralement pas d’un seul coup : des processus web, des workers de file d’attente, des tâches planifiées et des répliques exécutant des versions différentes peuvent coexister. Si une nouvelle version supprime une colonne qu’un ancien worker lit encore, ou si une colonne devient obligatoire avant que tous les écrivains ne la renseignent, le déploiement n’est plus compatible.

Les migrations de base de données sans interruption en PHP traitent le schéma et les données comme des composants d’un contrat opérationnel. L’objectif n’est pas seulement d’exécuter une instruction DDL correcte, mais de maintenir les lectures et les écritures disponibles pendant que les anciennes et nouvelles versions coexistent, et de conserver une voie de récupération réaliste.

Pourquoi le schéma peut casser un code déjà testé

Pourquoi le schéma peut casser un code déjà testé — guía visual de DedicatedPHP

Les tests locaux partent généralement d’une base de données créée de zéro ou mise à jour instantanément. Ce scénario omet la transition : données historiques incomplètes, millions de lignes, verrous, connexions persistantes et consommateurs asynchrones. Une modification apparemment mineure peut provoquer des erreurs ou une dégradation.

  • Renommer ou supprimer une colonne casse les requêtes, les mappeurs ORM, les rapports et les processus qui utilisent encore l’ancien nom.
  • Ajouter une contrainte NOT NULL échoue s’il existe d’anciennes lignes sans valeur ou si un écrivain ne connaît pas encore le nouveau champ.
  • Modifier un type peut tronquer des valeurs, altérer des comparaisons, invalider des index ou provoquer des conversions coûteuses.
  • Créer un index ou réécrire une grande table peut maintenir des verrous et augmenter la latence des opérations normales.
  • Une mise à jour massive dans une seule transaction peut épuiser le journal transactionnel, concurrencer les ressources ou compliquer la réplication.

La question pertinente est : quelles versions du code peuvent lire et écrire chaque représentation d’une donnée pendant toute la fenêtre de déploiement ? La réponse doit inclure les exécutables qui ne redémarrent pas automatiquement, et pas seulement les requêtes HTTP.

Compatibilité temporaire entre code, données et processus

Pendant un déploiement progressif, au moins trois états doivent être compatibles : ancien code, nouveau code et données aux formats ancien, nouveau ou partiellement transformé. La compatibilité ne consiste pas nécessairement à ce que chaque consommateur comprenne tous les formats pour toujours ; elle consiste à définir une fenêtre limitée dans laquelle les combinaisons prévisibles fonctionnent.

Par exemple, pour remplacer full_name par first_name et last_name, il ne convient pas de supprimer le champ d’origine au début. La nouvelle version peut écrire les deux formats et lire d’abord les nouveaux champs lorsqu’ils sont complets, avec un recours explicite à l’ancienne valeur. La version précédente continue de fonctionner avec full_name. Une fois l’historique transformé et les anciens consommateurs retirés, la lecture peut dépendre uniquement de la nouvelle structure.

Évitez que la compatibilité temporaire soit dispersée dans les contrôleurs. Centralisez la lecture, l’écriture et la normalisation dans un service de domaine ou un repository. Il devient ainsi possible d’auditer quelle version du format est produite, quelle valeur est prioritaire et quand retirer la logique transitoire. Un template de migration ne remplace pas ce modèle de compatibilité : le template exécute des changements ; le modèle définit le comportement de l’application pendant la transition.

Le modèle étendre, migrer et retirer

1. Étendre sans invalider les consommateurs actuels

La première phase ajoute des capacités sans supprimer celles qui existent : une colonne nullable, une nouvelle table, un index supplémentaire ou une structure parallèle. Elle doit éviter les changements destructifs et, lorsque le moteur l’exige, planifier la méthode de création afin de réduire les verrous. Ajouter une colonne n’implique pas qu’il soit sûr d’imposer immédiatement une valeur par défaut, de recalculer toutes les lignes ou de la déclarer obligatoire.

Avant d’exécuter l’opération, examinez la taille de la table, les requêtes les plus fréquentes, les clés étrangères, l’espace disponible, la charge de réplication et le comportement spécifique du moteur de base de données. Répétez l’opération sur une copie représentative ou dans un environnement présentant un volume et une concurrence comparables. Définissez également des limites observables : durée, latence admissible, taux d’erreurs et condition d’annulation.

2. Déployer des écrivains et lecteurs compatibles

Ensuite, du code qui comprend les deux représentations est déployé. Les nouveaux écrivains peuvent effectuer une double écriture si le coût et la cohérence le permettent. Les lecteurs doivent établir une priorité non ambiguë : lire la nouvelle valeur si elle est validée ; sinon, utiliser l’ancienne. N’utilisez pas une exception comme mécanisme de fallback, car elle masque les défauts de données et ajoute du travail inutile au chemin critique.

La double écriture exige des décisions explicites. Si une mise à jour affecte les deux structures, déterminez si elle doit être effectuée dans la même transaction. Si ce n’est pas possible, concevez une réconciliation idempotente et des métriques permettant de détecter les divergences. Les événements, caches, API et exportations sont également des consommateurs : modifier uniquement le repository PHP ne garantit pas une compatibilité de bout en bout.

3. Migrer l’historique de manière reprenable

Après avoir activé le code compatible, transformez les enregistrements existants par petits lots. Chaque lot doit pouvoir être répété sans dupliquer les effets ni corrompre les données. Utilisez une clé stable ou un curseur persistant, des limites de taille, l’enregistrement de la progression et des tentatives contrôlées. Évitez de paginer avec des décalages sur des ensembles qui changent, car cela peut sauter ou retraiter des lignes.

$lastId = 0; // Pour une clé primaire positive et croissante.

while (true) {
    $rows = $repository->findPendingAfterId($lastId, 500);

    if ($rows === []) {
        break;
    }

    foreach ($rows as $row) {
        $repository->migrateIfNeeded($row);
        $lastId = $row->id;
    }
}

Ce modèle exige que findPendingAfterId() renvoie des lignes triées par ordre croissant selon la même clé que celle utilisée comme curseur. Le curseur commence à une valeur antérieure au premier identifiant valide et avance seulement après le traitement de chaque ligne ; l’arrêt dépend du fait que la requête ne renvoie aucun lot. Lors d’une exécution reprise, la valeur confirmée de $lastId doit être persistée. migrateIfNeeded() doit vérifier l’état actuel et produire le même résultat s’il est exécuté à nouveau.

Mesurez les lignes en attente, les lignes transformées, les erreurs de validation et les différences entre les formats. Ne déclarez pas la phase terminée parce que la table a été parcourue : vérifiez également l’intégrité référentielle, l’unicité, les totaux métier et des échantillons d’enregistrements critiques.

4. Modifier les lectures, observer et retirer

Lorsque l’historique est complet et que les anciens processus ont cessé de s’exécuter, modifiez les lectures afin d’utiliser exclusivement la nouvelle structure. Cette activation peut être progressive au moyen d’une configuration contrôlée, mais elle ne doit pas être confondue avec le déploiement : déployer rend le code disponible ; activer modifie le chemin qu’utilise le trafic.

Observez les erreurs de requête, les champs nuls inattendus, les écarts fonctionnels, les temps de réponse et l’état des workers. Ce n’est qu’après une fenêtre d’observation définie que vous retirerez la double écriture, les dépendances transitoires et, enfin, l’ancienne colonne, l’ancien index ou l’ancienne table. Conserver indéfiniment des structures obsolètes augmente l’ambiguïté et le coût ; les supprimer trop tôt supprime la récupération simple.

Nulls, types, contraintes et index sans arrêter l’opération

Une nouvelle colonne commence généralement comme nullable parce que les enregistrements historiques ne l’ont pas encore. L’application doit traiter l’absence comme un état prévu, et non comme un cas impossible. Après avoir effectué et validé le backfill, une contrainte peut être imposée, à condition que tous les écrivains actifs fournissent une valeur valide.

Pour les changements de type, créez une nouvelle colonne et convertissez les valeurs de manière explicite. Cela permet de détecter les valeurs non convertibles, d’appliquer des règles d’arrondi ou de normalisation et de comparer les deux résultats avant de remplacer la colonne précédente. Modifier directement le type peut convenir dans des cas limités, mais cela doit être justifié par le comportement du moteur, le volume et la compatibilité des requêtes.

Les index nécessitent une analyse équivalente. Un nouvel index peut améliorer les lectures, mais sa construction consomme des ressources et une stratégie de création inadaptée peut bloquer les écritures. Validez le plan d’exécution de la requête qui en a besoin ; n’ajoutez pas d’index par intuition. Si le moteur propose des modes de création avec moins de blocages, comprenez leurs exigences et leurs limites avant de les intégrer au plan.

Rollback : le retour du code n’implique pas toujours le retour des données

Un rollback opérationnel doit être séparé en décisions. Tant que l’ancienne structure et la double écriture existent, il est généralement possible de revenir au code précédent. Mais si le nouveau format a accepté des informations que l’ancien modèle ne peut pas représenter, défaire le schéma ne récupère pas sémantiquement ces données.

  • Réversible : désactiver une nouvelle lecture et revenir au fallback, tout en conservant les deux structures.
  • Compensable : corriger ou reconstruire les données depuis une source définie, avec un processus audité.
  • Irréversible : supprimer une structure ou accepter des transformations qui perdent de la précision sans conserver l’original.

Documentez le point de non-retour, la personne chargée de l’autoriser, les copies ou exportations nécessaires et la procédure pour suspendre les workers. Une méthode down() dans un outil de migration n’est pas à elle seule un plan de rollback : elle peut inverser le DDL, mais ne garantit pas la validité des données écrites pendant la transition.

Tests, preuves et liste de vérification

Tests, preuves et liste de vérification — guía visual de DedicatedPHP

Testez une matrice de compatibilité : ancien code avec schéma étendu, nouveau code avec données pas encore migrées, nouveau code avec données transformées et processus asynchrones dans des versions mixtes. Incluez des migrations interrompues et reprises, des enregistrements invalides, la concurrence d’écriture et la restauration d’une version antérieure lorsque cela s’applique.

  • Inventorier les tables, requêtes, workers, intégrations et rapports concernés.
  • Définir le contrat temporaire de lecture et d’écriture, y compris les valeurs nulles et les priorités.
  • Séparer l’extension, le déploiement compatible, le backfill, l’activation et le retrait en étapes indépendantes.
  • Estimer l’impact du DDL, des index et des lots avec des données représentatives.
  • Rendre le processus de données idempotent, reprenable et mesurable.
  • Établir des validations d’intégrité et des seuils d’observation ultérieurs.
  • Documenter le rollback, les compensations et le point de non-retour.
  • Retirer la compatibilité et les anciennes structures uniquement avec la preuve qu’il ne reste aucun consommateur.

Appliqué avec discipline, ce modèle transforme un changement de base de données à haut risque en une séquence vérifiable. La clé consiste à concevoir la coexistence comme une partie du produit et de l’exploitation, et non comme un détail caché dans une migration.

Vous souhaitez appliquer ces idées à votre projet ?Parlons de votre plateforme PHP.
Afficher les services associés