Vai al contenuto
DedicatedPHP Contatto

Migrazioni di database senza interruzioni in PHP

Scoprite come evolvere gli schemi PHP con compatibilità temporanea, migrazioni riprendibili, validazione dei dati e rollback operativo.

Diagramma editoriale delle fasi espandere, migrare e rimuovere per modificare uno schema di database in un'applicazione PHP

Una migrazione dello schema può fallire anche se la modifica del codice ha superato i test. In produzione, un'applicazione di norma non cambia tutta insieme: possono coesistere processi web, worker di coda, job pianificati e repliche che eseguono versioni diverse. Se una nuova versione elimina una colonna che un vecchio worker legge ancora, o se una colonna diventa obbligatoria prima che tutti gli scrittori la popolino, il deployment non è più compatibile.

Le migrazioni di database senza interruzioni in PHP trattano lo schema e i dati come componenti di un contratto operativo. L'obiettivo non è soltanto eseguire una corretta istruzione DDL, ma mantenere disponibili letture e scritture mentre le versioni vecchia e nuova convivono, e conservare una concreta possibilità di ripristino.

Perché lo schema può rompere codice già testato

Perché lo schema può rompere codice già testato — guía visual de DedicatedPHP

I test locali in genere partono da un database creato da zero o aggiornato istantaneamente. Questo scenario omette la transizione: dati storici incompleti, milioni di righe, lock, connessioni persistenti e consumer asincroni. Una modifica apparentemente minore può causare errori o degrado delle prestazioni.

  • Rinominare o eliminare una colonna interrompe query, mapper ORM, report e processi che usano ancora il nome precedente.
  • Aggiungere un vincolo NOT NULL fallisce se esistono vecchie righe senza valore o se uno scrittore non conosce ancora il nuovo campo.
  • Modificare un tipo può troncare valori, alterare confronti, invalidare indici o provocare conversioni costose.
  • Creare un indice o riscrivere una tabella grande può mantenere lock e aumentare la latenza delle normali operazioni.
  • Un aggiornamento massivo in un'unica transazione può esaurire il log transazionale, competere per le risorse o rendere più difficile la replica.

La domanda rilevante è: quali versioni del codice possono leggere e scrivere ciascuna rappresentazione di un dato per l'intera finestra di deployment? La risposta deve includere gli eseguibili che non vengono riavviati automaticamente, non soltanto le richieste HTTP.

Compatibilità temporanea tra codice, dati e processi

Durante un deployment graduale esistono almeno tre stati che devono essere compatibili: codice vecchio, codice nuovo e dati con formati vecchio, nuovo o parzialmente trasformato. La compatibilità non consiste necessariamente nel fatto che ogni consumer comprenda tutti i formati per sempre; consiste nel definire una finestra delimitata in cui le combinazioni prevedibili funzionino.

Per esempio, per sostituire full_name con first_name e last_name, non è opportuno eliminare il campo originale all'inizio. La nuova versione può scrivere entrambi i formati e leggere prima i nuovi campi quando sono completi, con un fallback esplicito al valore precedente. La versione precedente continua a operare con full_name. Una volta trasformato lo storico e rimossi i vecchi consumer, la lettura può dipendere solo dalla nuova struttura.

Evitate che la compatibilità temporanea resti dispersa nei controller. Centralizzate lettura, scrittura e normalizzazione in un servizio di dominio o repository. In questo modo è possibile verificare quale versione del formato viene prodotta, quale valore ha priorità e quando rimuovere la logica transitoria. Un template di migrazione non sostituisce questo modello di compatibilità: il template esegue modifiche; il modello di compatibilità definisce come si comporta l'applicazione durante la transizione.

Il pattern espandere, migrare e rimuovere

1. Espandere senza invalidare i consumer attuali

La prima fase aggiunge capacità senza rimuovere quelle esistenti: una colonna nullable, una nuova tabella, un indice aggiuntivo o una struttura parallela. Deve evitare modifiche distruttive e, quando il motore lo richiede, pianificare il metodo di creazione per ridurre i lock. Aggiungere una colonna non implica che sia sicuro imporre subito un valore predefinito, ricalcolare tutte le righe o dichiararla obbligatoria.

Prima di eseguire l'operazione, esaminate le dimensioni della tabella, le query più frequenti, le chiavi esterne, lo spazio disponibile, il carico di replica e il comportamento specifico del motore di database. Fate prove su una copia rappresentativa o in un ambiente con volume e concorrenza comparabili. Definite inoltre limiti osservabili: durata, latenza ammissibile, tasso di errori e condizione di annullamento.

2. Rilasciare scrittori e lettori compatibili

Successivamente viene rilasciato codice che comprende le due rappresentazioni. I nuovi scrittori possono effettuare una scrittura duale se il costo e la consistenza lo consentono. I lettori devono stabilire una precedenza inequivocabile: leggere il nuovo valore se è validato; altrimenti, usare quello vecchio. Non usate un'eccezione come meccanismo di fallback, perché nasconde difetti nei dati e aggiunge lavoro non necessario al percorso critico.

La scrittura duale richiede decisioni esplicite. Se un aggiornamento riguarda entrambe le strutture, stabilite se debba essere eseguito nella stessa transazione. Se non è possibile, progettate una riconciliazione idempotente e metriche per rilevare divergenze. Anche eventi, cache, API ed esportazioni sono consumer: modificare solo il repository PHP non garantisce la compatibilità end-to-end.

3. Migrare lo storico in modo riprendibile

Dopo aver abilitato il codice compatibile, trasformate i record esistenti in piccoli batch. Ogni batch deve poter essere ripetuto senza duplicare effetti né corrompere dati. Usate una chiave stabile o un cursore persistente, limiti di dimensione, registrazione dell'avanzamento e retry controllati. Evitate di paginare con offset su insiemi che cambiano, poiché potreste saltare o rielaborare righe.

$lastId = 0; // Per una chiave primaria positiva e crescente.

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

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

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

Questo pattern richiede che findPendingAfterId() restituisca righe ordinate in modo crescente secondo la stessa chiave usata come cursore. Il cursore inizia con un valore precedente al primo identificatore valido e avanza solo dopo aver elaborato ogni riga; la terminazione dipende dal fatto che la query non restituisca alcun batch. In un'esecuzione ripresa, il valore confermato di $lastId deve essere persistito. migrateIfNeeded() deve verificare lo stato attuale e produrre lo stesso risultato se viene eseguito nuovamente.

Misurate righe in attesa, righe trasformate, errori di validazione e differenze tra formati. Non dichiarate conclusa la fase per aver attraversato la tabella: verificate anche l'integrità referenziale, l'unicità, i totali di business e campioni di record critici.

4. Modificare le letture, osservare e rimuovere

Quando lo storico è completo e i vecchi processi hanno smesso di essere eseguiti, modificate le letture affinché usino esclusivamente la nuova struttura. Questa attivazione può essere graduale tramite una configurazione controllata, ma non deve essere confusa con il deployment: il deployment rende disponibile il codice; l'attivazione modifica quale percorso utilizza il traffico.

Osservate errori di query, campi null imprevisti, discrepanze funzionali, tempi di risposta e stato di salute dei worker. Solo dopo una finestra di osservazione definita rimuovete la scrittura duale, le dipendenze transitorie e, infine, la vecchia colonna, il vecchio indice o la vecchia tabella. Conservare indefinitamente strutture obsolete aumenta ambiguità e costi; eliminarle troppo presto elimina il ripristino semplice.

Null, tipi, vincoli e indici senza fermare l'operatività

Una nuova colonna inizia spesso come nullable perché i record storici non la possiedono ancora. L'applicazione deve trattare l'assenza come uno stato previsto, non come un caso impossibile. Dopo aver completato e validato il backfill, può essere imposto un vincolo, purché tutti gli scrittori attivi forniscano un valore valido.

Per le modifiche di tipo, create una nuova colonna e convertite i valori in modo esplicito. Ciò permette di rilevare valori non convertibili, applicare regole di arrotondamento o normalizzazione e confrontare entrambi i risultati prima di sostituire la colonna precedente. Modificare direttamente il tipo può essere appropriato in casi limitati, ma deve essere giustificato dal comportamento del motore, dal volume e dalla compatibilità delle query.

Anche gli indici richiedono un'analisi equivalente. Un nuovo indice può migliorare le letture, ma la sua costruzione consuma risorse e una strategia di creazione inadeguata può bloccare le scritture. Validate il piano di esecuzione della query che ne ha bisogno; non aggiungete indici per intuizione. Se il motore offre modalità di creazione con minori lock, comprendetene requisiti e limitazioni prima di inserirle nel piano.

Rollback: il rollback del codice non implica sempre il rollback dei dati

Un rollback operativo deve essere separato in decisioni. Finché esistono la vecchia struttura e la scrittura duale, di norma è possibile tornare al codice precedente. Ma se il nuovo formato ha accettato informazioni che il vecchio modello non può rappresentare, annullare lo schema non recupera semanticamente tali dati.

  • Reversibile: disattivare una nuova lettura e tornare al fallback, mantenendo entrambe le strutture.
  • Compensabile: correggere o ricostruire dati da una fonte definita, con un processo verificabile.
  • Irreversibile: eliminare una struttura o accettare trasformazioni che perdono precisione senza conservare l'originale.

Documentate il punto di non ritorno, il responsabile dell'autorizzazione, le copie o esportazioni necessarie e la procedura per mettere in pausa i worker. Un metodo down() in uno strumento di migrazione non è di per sé un piano di rollback: può annullare il DDL, ma non garantisce la validità dei dati scritti durante la transizione.

Test, evidenze e checklist

Test, evidenze e checklist — guía visual de DedicatedPHP

Testate una matrice di compatibilità: codice vecchio con schema espanso, codice nuovo con dati non ancora migrati, codice nuovo con dati trasformati e processi asincroni in versioni miste. Includete migrazioni interrotte e riprese, record non validi, concorrenza in scrittura e ripristino di una versione precedente quando applicabile.

  • Inventariare tabelle, query, worker, integrazioni e report coinvolti.
  • Definire il contratto temporaneo di lettura e scrittura, inclusi valori null e priorità.
  • Separare espansione, deployment compatibile, backfill, attivazione e rimozione in passaggi indipendenti.
  • Stimare l'impatto di DDL, indici e batch con dati rappresentativi.
  • Rendere il processo sui dati idempotente, riprendibile e misurabile.
  • Stabilire validazioni di integrità e soglie di osservazione successive.
  • Documentare rollback, compensazioni e il punto di non ritorno.
  • Rimuovere la compatibilità e la struttura vecchie solo con evidenze che non restano consumer.

Applicato con disciplina, questo pattern trasforma una modifica al database ad alto rischio in una sequenza verificabile. La chiave è progettare la convivenza come parte del prodotto e dell'operatività, non come un dettaglio nascosto all'interno di una migrazione.

Vuoi applicare queste idee al tuo progetto?Parliamo della tua piattaforma PHP.
Visualizza il servizio correlato