Een schema-migratie kan mislukken, ook als de codewijziging de tests heeft doorstaan. In productie verandert een applicatie doorgaans niet in één keer: webprocessen, queue-workers, geplande taken en replica's die verschillende versies uitvoeren, kunnen naast elkaar bestaan. Als een nieuwe versie een kolom verwijdert die een oude worker nog leest, of als een kolom verplicht wordt voordat alle schrijvers die invullen, is de uitrol niet langer compatibel.
Database-migraties zonder onderbreking in PHP behandelen het schema en de gegevens als onderdelen van een operationeel contract. Het doel is niet alleen een correcte DDL-instructie uit te voeren, maar lees- en schrijfbewerkingen beschikbaar te houden terwijl de oude en nieuwe versies naast elkaar bestaan, en een realistische herstelmogelijkheid te behouden.
Waarom het schema reeds geteste code kan breken

Lokale tests gaan vaak uit van een database die vanaf nul is aangemaakt of onmiddellijk is bijgewerkt. Dat scenario laat de overgang buiten beschouwing: onvolledige historische gegevens, miljoenen rijen, vergrendelingen, persistente verbindingen en asynchrone afnemers. Een ogenschijnlijk kleine wijziging kan fouten of degradatie veroorzaken.
- Het hernoemen of verwijderen van een kolom breekt query's, ORM-mappers, rapporten en processen die nog de oude naam gebruiken.
- Het toevoegen van een
NOT NULL-beperking mislukt als oude rijen geen waarde hebben of als een schrijver het nieuwe veld nog niet kent. - Het wijzigen van een type kan waarden afkappen, vergelijkingen veranderen, indexen ongeldig maken of kostbare conversies veroorzaken.
- Het aanmaken van een index of het herschrijven van een grote tabel kan vergrendelingen vasthouden en de latentie van normale bewerkingen verhogen.
- Een bulkupdate in één enkele transactie kan het transactielog uitputten, concurreren om systeembronnen of replicatie bemoeilijken.
De relevante vraag is: welke versies van de code kunnen elke representatie van een gegeven lezen en schrijven gedurende het volledige uitrolvenster? Het antwoord moet ook uitvoerbare processen omvatten die niet automatisch herstarten, niet alleen HTTP-verzoeken.
Tijdelijke compatibiliteit tussen code, gegevens en processen
Tijdens een geleidelijke uitrol zijn er ten minste drie toestanden die compatibel moeten zijn: oude code, nieuwe code en gegevens met oude, nieuwe of gedeeltelijk getransformeerde formaten. Compatibiliteit betekent niet noodzakelijk dat elke afnemer voor altijd alle formaten begrijpt; het betekent dat een afgebakend venster wordt gedefinieerd waarin de voorzienbare combinaties werken.
Om bijvoorbeeld full_name te vervangen door first_name en last_name, is het niet raadzaam om het oorspronkelijke veld meteen te verwijderen. De nieuwe versie kan beide formaten schrijven en eerst de nieuwe velden lezen wanneer die volledig zijn, met een expliciete terugvaloptie naar de oude waarde. De vorige versie blijft werken met full_name. Zodra de historische gegevens zijn getransformeerd en de oude afnemers zijn uitgefaseerd, kan het lezen uitsluitend van de nieuwe structuur afhangen.
Voorkom dat de tijdelijke compatibiliteit verspreid raakt over controllers. Centraliseer lezen, schrijven en normalisatie in een domeinservice of repository. Zo kan worden geaudit welke versie van het formaat wordt geproduceerd, welke waarde voorrang heeft en wanneer de tijdelijke logica moet worden verwijderd. Een migratiesjabloon vervangt dit compatibiliteitsmodel niet: het sjabloon voert wijzigingen uit; het model definieert hoe de applicatie zich tijdens de overgang gedraagt.
Het patroon uitbreiden, migreren en verwijderen
1. Uitbreiden zonder huidige afnemers ongeldig te maken
De eerste fase voegt mogelijkheden toe zonder bestaande weg te nemen: een nullable kolom, een nieuwe tabel, een extra index of een parallelle structuur. Destructieve wijzigingen moeten worden vermeden en, wanneer de database-engine dat vereist, moet de aanmaakmethode worden gepland om vergrendelingen te beperken. Het toevoegen van een kolom betekent niet dat het veilig is om meteen een standaardwaarde op te leggen, alle rijen te herberekenen of de kolom verplicht te verklaren.
Controleer vóór het uitvoeren van de bewerking de tabelgrootte, de meest frequente query's, vreemde sleutels, beschikbare ruimte, replicatiebelasting en het specifieke gedrag van de database-engine. Test de procedure op een representatieve kopie of in een omgeving met vergelijkbaar volume en vergelijkbare gelijktijdigheid. Definieer ook meetbare limieten: duur, aanvaardbare latentie, foutenratio en annuleringsvoorwaarde.
2. Compatibele schrijvers en lezers uitrollen
Vervolgens wordt code uitgerold die beide representaties begrijpt. Nieuwe schrijvers kunnen dubbel schrijven als de kosten en consistentie dat toelaten. Lezers moeten een ondubbelzinnige voorrangsregel vastleggen: lees de nieuwe waarde als die gevalideerd is; gebruik anders de oude. Gebruik geen uitzondering als terugvalmechanisme, omdat dit datadefecten verbergt en onnodig werk toevoegt aan het kritieke pad.
Dubbel schrijven vereist expliciete beslissingen. Als een update beide structuren beïnvloedt, bepaal dan of die in dezelfde transactie moet plaatsvinden. Als dat niet mogelijk is, ontwerp dan een idempotente reconciliatie en meetwaarden om verschillen te detecteren. Gebeurtenissen, caches, API's en exporten zijn ook afnemers: alleen de PHP-repository wijzigen garandeert geen end-to-endcompatibiliteit.
3. Historische gegevens hervatbaar migreren
Transformeer na het inschakelen van de compatibele code de bestaande gegevens in kleine batches. Elke batch moet herhaald kunnen worden zonder effecten te dupliceren of gegevens te beschadigen. Gebruik een stabiele sleutel of een persistente cursor, groottelimieten, voortgangsregistratie en gecontroleerde herhaalpogingen. Vermijd paginering met offsets over veranderende gegevensverzamelingen, omdat daarbij rijen kunnen worden overgeslagen of opnieuw verwerkt.
$lastId = 0; // Voor een positieve en oplopende primaire sleutel.
while (true) {
$rows = $repository->findPendingAfterId($lastId, 500);
if ($rows === []) {
break;
}
foreach ($rows as $row) {
$repository->migrateIfNeeded($row);
$lastId = $row->id;
}
}Dit patroon vereist dat findPendingAfterId() rijen teruggeeft die oplopend zijn gesorteerd op dezelfde sleutel die als cursor wordt gebruikt. De cursor begint met een waarde vóór de eerste geldige identifier en gaat pas verder nadat elke rij is verwerkt; de beëindiging hangt ervan af dat de query geen batch teruggeeft. Bij een hervatte uitvoering moet de bevestigde waarde van $lastId worden gepersisteerd. migrateIfNeeded() moet de huidige status controleren en hetzelfde resultaat opleveren als de functie opnieuw wordt uitgevoerd.
Meet openstaande rijen, getransformeerde rijen, validatiefouten en verschillen tussen formaten. Verklaar de fase niet als voltooid omdat de tabel is doorlopen: controleer ook referentiële integriteit, uniciteit, zakelijke totalen en steekproeven van kritieke gegevens.
4. Leesbewerkingen omschakelen, observeren en verwijderen
Wanneer de historische gegevens volledig zijn en de oude processen niet meer worden uitgevoerd, schakelt u de leesbewerkingen om zodat uitsluitend de nieuwe structuur wordt gebruikt. Deze activering kan geleidelijk gebeuren via een gecontroleerde configuratie, maar mag niet worden verward met de uitrol: uitrollen maakt code beschikbaar; activeren wijzigt welk pad door het verkeer wordt gebruikt.
Observeer queryfouten, onverwachte null-waarden, functionele discrepanties, responstijden en de gezondheid van workers. Verwijder pas na een gedefinieerd observatievenster dubbel schrijven, tijdelijke afhankelijkheden en uiteindelijk de oude kolom, index of tabel. Verouderde structuren onbeperkt behouden verhoogt ambiguïteit en kosten; ze te vroeg verwijderen elimineert eenvoudig herstel.
Nulls, typen, beperkingen en indexen zonder de werking te stoppen
Een nieuwe kolom begint doorgaans als nullable omdat historische gegevens deze nog niet hebben. De applicatie moet de afwezigheid behandelen als een verwachte status, niet als een onmogelijk geval. Na het voltooien en valideren van de backfill kan een beperking worden opgelegd, mits alle actieve schrijvers een geldige waarde aanleveren.
Maak bij typewijzigingen een nieuwe kolom en converteer de waarden expliciet. Dit maakt het mogelijk niet-converteerbare waarden te detecteren, afrondings- of normalisatieregels toe te passen en beide resultaten te vergelijken voordat de vorige kolom wordt vervangen. Het type rechtstreeks wijzigen kan in beperkte gevallen passend zijn, maar moet worden gerechtvaardigd door het gedrag van de engine, het volume en de compatibiliteit van de query's.
Indexen vereisen een gelijkwaardige analyse. Een nieuwe index kan leesbewerkingen verbeteren, maar de opbouw ervan verbruikt systeembronnen en een ongeschikte aanmaakstrategie kan schrijfbewerkingen blokkeren. Valideer het uitvoeringsplan van de query die de index nodig heeft; voeg geen indexen op intuïtie toe. Als de engine aanmaakmodi met minder blokkering biedt, begrijp dan de vereisten en beperkingen voordat u ze in het plan opneemt.
Rollback: het terugdraaien van code betekent niet altijd het terugdraaien van gegevens
Een operationele rollback moet worden opgesplitst in beslissingen. Zolang de oude structuur en dubbel schrijven bestaan, is het doorgaans mogelijk om terug te keren naar de vorige code. Maar als het nieuwe formaat informatie heeft geaccepteerd die het oude model niet kan representeren, herstelt het terugdraaien van het schema die gegevens niet semantisch.
- Omkeerbaar: een nieuwe leesbewerking uitschakelen en terugkeren naar de terugvaloptie, terwijl beide structuren behouden blijven.
- Compenseerbaar: gegevens corrigeren of reconstrueren vanuit een gedefinieerde bron, met een auditbaar proces.
- Onomkeerbaar: een structuur verwijderen of transformaties accepteren die precisie verliezen zonder het origineel te bewaren.
Documenteer het onomkeerbare punt, de verantwoordelijke die het mag autoriseren, de vereiste kopieën of exporten en de procedure om workers te pauzeren. Een down()-methode in een migratietool is op zichzelf geen rollbackplan: deze kan DDL terugdraaien, maar garandeert niet de geldigheid van de gegevens die tijdens de overgang zijn geschreven.
Tests, bewijsmateriaal en checklist

Test een compatibiliteitsmatrix: oude code met uitgebreid schema, nieuwe code met nog niet gemigreerde gegevens, nieuwe code met getransformeerde gegevens en asynchrone processen met gemengde versies. Neem onderbroken en hervatte migraties, ongeldige gegevens, gelijktijdige schrijfbewerkingen en het herstellen van een eerdere versie, wanneer van toepassing, op.
- Inventariseer betrokken tabellen, query's, workers, integraties en rapporten.
- Definieer het tijdelijke lees- en schrijfcontract, inclusief null-waarden en prioriteiten.
- Scheid uitbreiding, compatibele uitrol, backfill, activering en verwijdering in onafhankelijke stappen.
- Schat de impact van DDL, indexen en batches met representatieve gegevens.
- Maak het dataproces idempotent, hervatbaar en meetbaar.
- Stel integriteitsvalidaties en drempelwaarden voor latere observatie vast.
- Documenteer rollback, compensaties en het onomkeerbare punt.
- Verwijder de oude compatibiliteit en structuur alleen wanneer er bewijs is dat er geen afnemers meer zijn.
Met discipline toegepast verandert dit patroon een databasewijziging met hoog risico in een verifieerbare reeks. De sleutel is om de co-existentie te ontwerpen als onderdeel van het product en de operatie, niet als een verborgen detail binnen een migratie.



