Il contesto

Sul backend di serignesam.blog, stavo aggiungendo il login Google: una semplice proprietà googleId sull'entità User. Il tipo di modifica che richiede due minuti... in teoria.

// src/Entity/User.php
#[ORM\Column(length: 255, nullable: true)]
private ?string $googleId = null;

Il sintomo

Au momento di sincronizzare lo schema:

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`))

UUn errore di vincolo sulla tabella commentaire... mentre avevo toccato solo User, e la mia colonna è nullable. Nessun rapporto apparente tra la mia modifica e il fallimento.

Capire: schema:update non fa mai «solo» la vostra modifica

È il punto che questo bug mi ha fatto assimilare una volta per tutte: doctrine:schema:update non applica la vostra modifica — calcola il diff completo tra il mapping delle entità e lo stato reale del database, poi tenta di riconciliare tutto in un colpo. Un --dump-sql lo mostra nero su bianco:

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

Il database era derivato dal mapping: un vincolo FK su commentaire.user_id era saltato (probabilmente durante una vecchia manipolazione manuale), e Doctrine voleva ricrearlo. Peccato che la tabella contenesse delle righe orfane — dei user_id che puntavano a utenti eliminati. MySQL rifiuta di creare una FK su dati che la violano già: l'intero batch fallisce, inclusa la mia innocente colonna.

Il correttivo immediato: l'ALTER mirato

UPer sbloccare la feature senza aprire il cantiere degli orfani nell'urgenza, l'aggiunta manuale e chirurgica della sola colonna voluta:

ALTER TABLE `user` ADD COLUMN `google_id` VARCHAR(255) NULL;

La colonna corrisponde esattamente al mapping: Doctrine la riconosce, il login Google funziona, e il problema della FK resta isolato invece di bloccare tutto lo sviluppo.

La vera pulizia: identificare e poi eliminare gli orfani

Poi, a freddo, affrontare la causa. Prima misurare l'ampiezza:

-- 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;

Poi decidere la loro sorte secondo il dominio — qui, conservare i commenti anonimizzandoli invece di eliminarli:

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;

UUna volta sani i dati, il vincolo si ricrea senza resistenza — e ne ho approfittato per definire il comportamento futuro con ON DELETE SET NULL, affinché l'eliminazione di un utente non ricrei mai più orfani.

La lezione di fondo: migrazioni, non schema:update

Questo bug è l'argomento definitivo per doctrine:migrations in produzione: una migrazione è un file esplicito e riletto che contiene solo le modifiche volute, versionato con il codice. schema:update --force, invece, applica un diff opaco calcolato all'istante T — incluse delle «riparazioni» che non sono mai state richieste, su tabelle che non sono mai state toccate.

php bin/console make:migration      # genere un fichier qu'on LIT avant d'appliquer
php bin/console doctrine:migrations:migrate

Quello che mi porto a casa

  • schema:update applica il diff completo mapping/database, mai solo la vostra modifica: qualsiasi deriva vecchia del database può far fallire una modifica senza rapporto.
  • --dump-sql prima di ogni --force, sistematicamente: è lì che si scoprono le modifiche incluse che non sono state richieste.
  • UUna FK impossibile da creare = dati che la violano già; il LEFT JOIN ... WHERE u.id IS NULL è la query riflesso per elencare gli orfani.
  • Sbloccare con un ALTER mirato, pulire a freddo, poi passare alle migrazioni: l'urgenza e l'igiene di base sono due cantieri separati.
  • ON DELETE SET NULL (o CASCADE, secondo il dominio) definito fin dalla progettazione evita che gli orfani riappaiano.