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:updateapplica il diff completo mapping/database, mai solo la vostra modifica: qualsiasi deriva vecchia del database può far fallire una modifica senza rapporto.--dump-sqlprima 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.