Le contexte
Sur le backend de serignesam.blog, j'ajoutais le login Google : une simple propriété googleId sur l'entité User. Le genre de modification qui prend deux minutes... en théorie.
// src/Entity/User.php
#[ORM\Column(length: 255, nullable: true)]
private ?string $googleId = null;
Le symptôme
Au moment de synchroniser le schéma :
php bin/console doctrine:schema:update --force
# SQLSTATE[23000]: Integrity constraint violation: 1452
# Cannot add or update a child row: a foreign key constraint fails
# (`ssam`.`commentaire`, CONSTRAINT `FK_...` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`))
Une erreur de contrainte sur la table commentaire... alors que je n'ai touché qu'à User, et que ma colonne est nullable. Aucun rapport apparent entre ma modification et l'échec.
Comprendre : schema:update ne fait jamais « juste » votre changement
C'est le point que ce bug m'a fait intégrer une bonne fois : doctrine:schema:update ne applique pas votre modification — il calcule le diff complet entre le mapping des entités et l'état réel de la base, puis tente de tout réconcilier d'un coup. Un --dump-sql le montre noir sur blanc :
php bin/console doctrine:schema:update --dump-sql
# ALTER TABLE user ADD google_id VARCHAR(255) DEFAULT NULL; ← mon changement
# ALTER TABLE commentaire ADD CONSTRAINT FK_... FOREIGN KEY ...; ← surprise : une FK à recréer
La base avait dérivé du mapping : une contrainte FK sur commentaire.user_id avait sauté (probablement lors d'une manipulation manuelle ancienne), et Doctrine voulait la recréer. Sauf que la table contenait des lignes orphelines — des user_id pointant vers des utilisateurs supprimés. MySQL refuse de créer une FK sur des données qui la violent déjà : tout le batch échoue, y compris mon innocente colonne.
Le correctif immédiat : l'ALTER ciblé
Pour débloquer la feature sans ouvrir le chantier des orphelins dans l'urgence, l'ajout manuel et chirurgical de la seule colonne voulue :
ALTER TABLE `user` ADD COLUMN `google_id` VARCHAR(255) NULL;
La colonne correspond exactement au mapping : Doctrine la reconnaît, le login Google fonctionne, et le problème de FK reste isolé au lieu de bloquer tout le développement.
Le vrai nettoyage : identifier puis purger les orphelins
Ensuite, à froid, traiter la cause. D'abord mesurer l'ampleur :
-- Quelles lignes de commentaire pointent vers des users disparus ?
SELECT c.id, c.user_id
FROM commentaire c
LEFT JOIN `user` u ON u.id = c.user_id
WHERE c.user_id IS NOT NULL AND u.id IS NULL;
Puis décider de leur sort selon le métier — ici, conserver les commentaires en les anonymisant plutôt que les supprimer :
UPDATE commentaire c
LEFT JOIN `user` u ON u.id = c.user_id
SET c.user_id = NULL
WHERE c.user_id IS NOT NULL AND u.id IS NULL;
Une fois les données saines, la contrainte se recrée sans résistance — et j'en ai profité pour définir le comportement futur avec ON DELETE SET NULL, pour que la suppression d'un utilisateur ne recrée plus jamais d'orphelins.
La leçon de fond : migrations, pas schema:update
Ce bug est l'argument définitif pour doctrine:migrations en production : une migration est un fichier explicite et relu qui ne contient que les changements voulus, versionné avec le code. schema:update --force, lui, applique un diff opaque calculé à l'instant T — y compris des « réparations » qu'on n'a jamais demandées, sur des tables qu'on n'a jamais touchées.
php bin/console make:migration # genere un fichier qu'on LIT avant d'appliquer
php bin/console doctrine:migrations:migrate
Ce que je retiens
schema:updateapplique le diff complet mapping/base, jamais seulement votre changement : toute dérive ancienne de la base peut faire échouer une modification sans rapport.--dump-sqlavant tout--force, systématiquement : c'est là qu'on découvre les changements embarqués qu'on n'a pas demandés.- Une FK impossible à créer = des données qui la violent déjà ; le
LEFT JOIN ... WHERE u.id IS NULLest la requête réflexe pour lister les orphelins. - Débloquer avec un ALTER ciblé, nettoyer à froid, puis passer aux migrations : l'urgence et l'hygiène de base sont deux chantiers séparés.
ON DELETE SET NULL(ou CASCADE, selon le métier) défini dès la conception évite que les orphelins ne réapparaissent.