Migracja schematu może zakończyć się niepowodzeniem, nawet jeśli zmiana kodu przeszła testy. Na produkcji aplikacja zwykle nie zmienia się jednocześnie: mogą współistnieć procesy webowe, workery kolejek, zadania harmonogramu i repliki uruchamiające różne wersje. Jeśli nowa wersja usuwa kolumnę, którą stary worker nadal odczytuje, albo jeśli kolumna staje się wymagana, zanim wszyscy zapisujący zaczną ją wypełniać, wdrożenie przestaje być kompatybilne.
Migracje baz danych bez przestojów w PHP traktują schemat i dane jako komponenty kontraktu operacyjnego. Celem nie jest wyłącznie wykonanie poprawnej instrukcji DDL, lecz utrzymanie dostępności odczytów i zapisów, gdy współistnieją stare i nowe wersje, oraz zachowanie realistycznej ścieżki odzyskania.
Dlaczego schemat może zepsuć już przetestowany kod

Testy lokalne zwykle bazują na bazie danych utworzonej od zera albo zaktualizowanej natychmiastowo. Taki scenariusz pomija przejście: niekompletne dane historyczne, miliony wierszy, blokady, trwałe połączenia i asynchronicznych konsumentów. Pozornie niewielka modyfikacja może powodować błędy lub degradację.
- Zmiana nazwy lub usunięcie kolumny psuje zapytania, mapery ORM, raporty i procesy nadal używające poprzedniej nazwy.
- Dodanie ograniczenia
NOT NULLkończy się niepowodzeniem, jeśli istnieją stare wiersze bez wartości lub jeśli zapisujący nadal nie zna nowego pola. - Zmiana typu może obciąć wartości, zmienić porównania, unieważnić indeksy lub wywołać kosztowne konwersje.
- Utworzenie indeksu lub przepisanie dużej tabeli może utrzymywać blokady i zwiększać opóźnienia zwykłych operacji.
- Masowa aktualizacja w jednej transakcji może wyczerpać dziennik transakcyjny, rywalizować o zasoby lub utrudnić replikację.
Istotne pytanie brzmi: które wersje kodu mogą odczytywać i zapisywać każdą reprezentację danych przez całe okno wdrożenia? Odpowiedź musi obejmować pliki wykonywalne, które nie są automatycznie restartowane, a nie tylko żądania HTTP.
Czasowa kompatybilność między kodem, danymi i procesami
Podczas stopniowego wdrożenia istnieją co najmniej trzy stany, które muszą być kompatybilne: stary kod, nowy kod oraz dane w formatach starym, nowym lub częściowo przekształconym. Kompatybilność nie musi oznaczać, że każdy konsument rozumie wszystkie formaty na zawsze; polega na zdefiniowaniu ograniczonego okna, w którym przewidywalne kombinacje działają.
Na przykład, aby zastąpić full_name przez first_name i last_name, nie należy usuwać oryginalnego pola na początku. Nowa wersja może zapisywać oba formaty i najpierw odczytywać nowe pola, gdy są kompletne, z wyraźnym fallbackiem do starej wartości. Poprzednia wersja nadal działa z full_name. Po przekształceniu danych historycznych i wycofaniu starych konsumentów odczyt może zależeć wyłącznie od nowej struktury.
Unikaj rozproszenia czasowej kompatybilności po kontrolerach. Scentralizuj odczyt, zapis i normalizację w usłudze domenowej lub repozytorium. Dzięki temu można audytować, która wersja formatu jest tworzona, która wartość ma priorytet i kiedy usunąć logikę przejściową. Szablon migracji nie zastępuje tego modelu kompatybilności: szablon wykonuje zmiany; model określa, jak aplikacja zachowuje się podczas przejścia.
Wzorzec rozszerzania, migracji i wycofywania
1. Rozszerzanie bez unieważniania obecnych konsumentów
Pierwsza faza dodaje możliwości bez usuwania istniejących: kolumnę nullable, nową tabelę, dodatkowy indeks lub strukturę równoległą. Należy unikać zmian destrukcyjnych i, gdy wymaga tego silnik, zaplanować metodę tworzenia w celu ograniczenia blokad. Dodanie kolumny nie oznacza, że bezpieczne jest natychmiastowe wymuszenie wartości domyślnej, przeliczenie wszystkich wierszy lub zadeklarowanie jej jako wymaganej.
Przed wykonaniem operacji sprawdź rozmiar tabeli, najczęstsze zapytania, klucze obce, dostępną przestrzeń, obciążenie replikacji i specyficzne zachowanie silnika bazy danych. Przećwicz to na reprezentatywnej kopii lub w środowisku o porównywalnym wolumenie i współbieżności. Zdefiniuj również obserwowalne limity: czas trwania, dopuszczalne opóźnienie, wskaźnik błędów i warunek anulowania.
2. Wdrożenie kompatybilnych zapisujących i odczytujących
Następnie wdrażany jest kod rozumiejący obie reprezentacje. Nowe komponenty zapisujące mogą wykonywać dual write, jeśli pozwalają na to koszt i spójność. Komponenty odczytujące muszą ustanowić jednoznaczny priorytet: odczytać nową wartość, jeśli jest zwalidowana; w przeciwnym razie użyć starej. Nie używaj wyjątku jako mechanizmu fallbacku, ponieważ ukrywa on defekty danych i dodaje niepotrzebną pracę do ścieżki krytycznej.
Dual write wymaga jawnych decyzji. Jeśli aktualizacja dotyczy obu struktur, ustal, czy musi zostać wykonana w tej samej transakcji. Jeśli nie jest to możliwe, zaprojektuj idempotentne uzgadnianie i metryki do wykrywania rozbieżności. Zdarzenia, cache, API i eksporty również są konsumentami: zmiana wyłącznie repozytorium PHP nie gwarantuje kompatybilności end-to-end.
3. Migracja danych historycznych w sposób wznawialny
Po włączeniu kompatybilnego kodu przekształcaj istniejące rekordy w małych partiach. Każda partia musi dać się powtórzyć bez dublowania skutków ani uszkadzania danych. Użyj stabilnego klucza lub trwałego kursora, limitów rozmiaru, rejestrowania postępu i kontrolowanych ponowień. Unikaj paginacji z offsetem po zmieniających się zbiorach, ponieważ może pomijać lub ponownie przetwarzać wiersze.
$lastId = 0; // Dla dodatniego i rosnącego klucza głównego.
while (true) {
$rows = $repository->findPendingAfterId($lastId, 500);
if ($rows === []) {
break;
}
foreach ($rows as $row) {
$repository->migrateIfNeeded($row);
$lastId = $row->id;
}
}Ten wzorzec wymaga, aby findPendingAfterId() zwracało wiersze posortowane rosnąco według tego samego klucza użytego jako kursor. Kursor rozpoczyna się od wartości wcześniejszej niż pierwszy prawidłowy identyfikator i przesuwa się dopiero po przetworzeniu każdego wiersza; zakończenie zależy od tego, czy zapytanie nie zwróci żadnej partii. Przy wznowionym wykonaniu potwierdzona wartość $lastId musi zostać utrwalona. migrateIfNeeded() musi sprawdzać bieżący stan i dawać ten sam wynik, gdy zostanie uruchomione ponownie.
Mierz liczbę oczekujących wierszy, przekształconych wierszy, błędów walidacji i różnic między formatami. Nie uznawaj fazy za zakończoną tylko dlatego, że tabela została przejrzana: sprawdź również integralność referencyjną, unikalność, sumy biznesowe i próbki krytycznych rekordów.
4. Przełączenie odczytów, obserwacja i wycofanie
Gdy dane historyczne są kompletne, a stare procesy przestały działać, przełącz odczyty tak, aby używały wyłącznie nowej struktury. Ta aktywacja może być stopniowa za pomocą kontrolowanej konfiguracji, lecz nie należy jej mylić z wdrożeniem: wdrożenie udostępnia kod; aktywacja zmienia ścieżkę używaną przez ruch.
Obserwuj błędy zapytań, nieoczekiwane pola null, rozbieżności funkcjonalne, czasy odpowiedzi i stan workerów. Dopiero po zdefiniowanym oknie obserwacji usuń dual write, zależności przejściowe, a na końcu starą kolumnę, indeks lub tabelę. Utrzymywanie przestarzałych struktur w nieskończoność zwiększa niejednoznaczność i koszt; usunięcie ich zbyt wcześnie eliminuje proste odzyskanie.
Wartości null, typy, ograniczenia i indeksy bez zatrzymywania działania
Nowa kolumna zwykle zaczyna jako nullable, ponieważ rekordy historyczne jeszcze jej nie mają. Aplikacja musi traktować brak jako przewidziany stan, a nie jako przypadek niemożliwy. Po ukończeniu i zwalidowaniu backfillu można narzucić ograniczenie, pod warunkiem że wszystkie aktywne komponenty zapisujące dostarczają prawidłową wartość.
W przypadku zmian typu utwórz nową kolumnę i konwertuj wartości w sposób jawny. Pozwala to wykrywać wartości niekonwertowalne, stosować reguły zaokrąglania lub normalizacji oraz porównywać oba wyniki przed zastąpieniem poprzedniej kolumny. Bezpośrednia zmiana typu może być odpowiednia w ograniczonych przypadkach, ale musi być uzasadniona zachowaniem silnika, wolumenem i kompatybilnością zapytań.
Indeksy wymagają równoważnej analizy. Nowy indeks może poprawić odczyty, lecz jego budowa zużywa zasoby, a nieodpowiednia strategia tworzenia może blokować zapisy. Zweryfikuj plan wykonania zapytania, które go potrzebuje; nie dodawaj indeksów intuicyjnie. Jeśli silnik oferuje tryby tworzenia z mniejszym blokowaniem, poznaj ich wymagania i ograniczenia, zanim włączysz je do planu.
Rollback: wycofanie kodu nie zawsze oznacza wycofanie danych
Rollback operacyjny należy rozdzielić na decyzje. Dopóki istnieje stara struktura i dual write, zwykle można wrócić do poprzedniego kodu. Jednak jeśli nowy format przyjął informacje, których stary model nie potrafi reprezentować, cofnięcie schematu nie odzyskuje semantycznie tych danych.
- Odwracalne: wyłączenie nowego odczytu i powrót do fallbacku, przy zachowaniu obu struktur.
- Kompensowalne: poprawienie lub odtworzenie danych ze zdefiniowanego źródła, za pomocą audytowanego procesu.
- Nieodwracalne: usunięcie struktury lub zaakceptowanie transformacji, które tracą precyzję bez zachowania oryginału.
Udokumentuj punkt bez powrotu, osobę odpowiedzialną za jego zatwierdzenie, niezbędne kopie lub eksporty oraz procedurę wstrzymania workerów. Metoda down() w narzędziu migracyjnym sama w sobie nie jest planem rollbacku: może cofnąć DDL, ale nie gwarantuje poprawności danych zapisanych podczas przejścia.
Testy, dowody i lista kontrolna

Przetestuj macierz kompatybilności: stary kod z rozszerzonym schematem, nowy kod z danymi jeszcze niezmigrowanymi, nowy kod z przekształconymi danymi oraz procesy asynchroniczne w mieszanych wersjach. Uwzględnij przerwane i wznowione migracje, nieprawidłowe rekordy, współbieżność zapisu i przywrócenie poprzedniej wersji, gdy ma zastosowanie.
- Zinwentaryzuj tabele, zapytania, workery, integracje i raporty, których dotyczy zmiana.
- Zdefiniuj czasowy kontrakt odczytu i zapisu, w tym wartości null i priorytety.
- Rozdziel rozszerzenie, kompatybilne wdrożenie, backfill, aktywację i wycofanie na niezależne kroki.
- Oszacuj wpływ DDL, indeksów i partii na reprezentatywnych danych.
- Uczyń proces danych idempotentnym, wznawialnym i mierzalnym.
- Ustal walidacje integralności oraz progi późniejszej obserwacji.
- Udokumentuj rollback, kompensacje i punkt bez powrotu.
- Usuwaj starą kompatybilność i strukturę wyłącznie mając dowody, że nie pozostali żadni konsumenci.
Stosowany z dyscypliną wzorzec ten zmienia wysokiego ryzyka zmianę bazy danych w weryfikowalną sekwencję. Kluczem jest zaprojektowanie współistnienia jako części produktu i operacji, a nie jako ukrytego szczegółu wewnątrz migracji.



