Jeżeli na co dzień pracujesz z frameworkiem Symfony, to myślę, iż ikonicznego Doctrine Migrations Bundle nie muszę Ci przedstawiać. o ile jednak pierwszy raz się spotykasz z tą nazwą, to śpieszę z wyjaśnieniem, iż jest to paczka kompatybilna z frameworkiem Symfony i Doctrine ORM, dzięki której możemy uruchamiać, tworzyć i zarządzać migracjami bazodanowymi.
W tym artykule przedstawię problem, który może zaistnieć w aplikacjach używających doctrine/migrations-bundle. Ten problem zwykle będzie dotyczył projektów bazujących na kodzie zewnętrznych platform, które ze względu na dynamicznie zmieniające się wymagania interesariuszy, muszą modyfikować kod dostawców 3rd party.
- Czym są migracje bazodanowe?
- Tło problemu
- Migracje aplikacji
- Migracje aplikacji vs. migracje platformy
- Migracje 3rd party
- Migracje on-top innych migracji
- Problem sortowania migracji
- Problem
- Rozwiązanie
- Inne problemy i rozwiązania
- Podsumowanie
Czym są migracje bazodanowe?
W cyklu wytwarzania systemu aplikacji, która jest połączoną z bazą danych, zupełnie naturalnym zjawiskiem jest fakt, iż schemat bazy danych będzie się zmieniał. Migracje bazodanowe są mechanizmem, który umożliwia kontrolowane i automatyczne wprowadzanie zmian w strukturze bazy danych. Dzięki migracjom możemy definiować i śledzić wersje schematu bazy danych w postaci plików migracyjnych. Umożliwia nam to tworzenie, aktualizację i cofanie zmian w bazie danych w sposób uporządkowany i bezpieczny. Aby móc te zmiany później zaaplikować na środowiskach o różnym przeznaczeniu, czyli np. stagingowym, produkcyjnym, jak i również na środowiskach twoich kolegów z biurka obok, musisz napisać migrację. Migracja ta zostanie później uruchomiona dzięki narzędzia takiego jak doctrine-migrations-bundle.
Nie wiesz czym są migracje bazodanowe w Doctrine ORM?
W tym artykule skupię się na pewnym bardzo konkretnym problemie w ramach migracji bazodanowych przy użyciu doctrine/migrations-bundle i zakładam, iż wiesz w jaki sposób to działa.
Jeżeli jednak chcesz dowiedzieć się więcej na ten temat, to chciałbym Ci serdecznie polecić artykuł Marcina Kuklińskiego, który w bardzo przystępny sposób może wprowadzić Cię w temat w ramach poniższego artykułu:

Każda migracja, którą stworzymy, jest potem wersjonowana. Zazwyczaj, wersjonowanie to odbywa się poprzez dodanie do nazwy klasy daty, w której została ona stworzona. Na przykład migracja może wyglądać w sposób następujący:
Nazwa klasy migracji, będzie wskazywać na to, iż jest to migracja, która została utworzona 15 października 2020 roku o godzinie 12:12:24. Wraz z tym, jak aplikacja powstaje, dochodzą do niej kolejne migracje.
Tło problemu
Rozważmy zatem aplikację, która łączy się z bazą danych SQL. Aplikacja ta jest oparta o framework Symfony i Doctrine ORM. Aplikacja ta posiada już pewne migracje.
Migracje aplikacji
Ze względu na to, iż nie chcę nadwerężać Twoich oczu, to pozwolę sobie skrócić nieco daty w nazwach tych migracji ( ͡° ͜ʖ ͡°).
Standardowo doctrine-migrations-bundle uruchomi migracje posortowane w sposób alfabetyczny:
Prosta sprawa! Takie podejście może jak najbardziej działać przez lata i nie będzie z tym absolutnie żadnego problemu.
Migracje aplikacji vs. migracje platformy
Załóżmy, iż aplikacja, którą rozważamy, bazuje na jakiejś platformie, takiej jak na przykład Sylius czy Akeneo. Platformy te dostarczają nam masę przydatnych funkcjonalności dostępnych out of the box. Oprócz tego posiadają również własne migracje bazujące na doctrine-migrations-bundle ( ͡° ͜ʖ ͡°)…
Okazuje się więc, iż nasza aplikacja musi uruchomić następujące migracje:
Doctrine Migrations Bundle uruchomi to w następującej kolejności:
Migracje 3rd party
Żeby było ciekawiej, załóżmy, iż do naszej platformy doinstalowaliśmy sobie jakiś plugin 3rd party. Ten plugin również posiada swoje własne migracje.
Migracje zostaną uruchomione w następującej kolejności:
Zastosowane podejście wciąż się sprawdza. Migracje, które zostały dodane z perspektywy aplikacji, wciąż są uporządkowane w sposób prawidłowy.
Aplikacja od dawna została już zdeployowana na produkcję. Wszyscy zarabiają pieniądze i są szczęśliwi. Aż tu nagle…
Migracje on-top innych migracji
Developer pracujący nad naszą hipotetyczną aplikacją otrzymuje nowe zadanie. Zadanie to polega na zmodyfikowaniu sposobu działania pluginu, który został wcześniej zainstalowany. Aby zrealizować zadanie, musi on zaingerować w schemę bazy danych, utworzoną wcześniej przez plugin.
Kolejność wykonania migracji wygląda w sposób następujący:
Kolejność wykonywania migracji wciąż jest prawidłowa!
Problem sortowania migracji
Czas mija nieubłaganie, a w aplikacji przybywa coraz to więcej migracji. Przezorni developerzy cyklicznie aktualizują platformę i pluginy do najnowszych wersji. Ze względu na to, iż migracje były uruchamiane wyłącznie na ich lokalnych środowiskach oraz dzięki CI/CD na systemie produkcyjnym i stagingowym, nikt nie zwrócił uwagi na to, iż kolejność wykonywania migracji wygląda teraz w sposób następujący:
Wprawione oko obserwatora zauważy, iż migracje, które zostały uruchomione, nie są posortowane po dacie, ale alfabetycznynie po nazwach namespace’ów klas migracji.
Załóżmy teraz, iż nowa migracja plugina, który zainstalowaliśmy, robi następującą rzecz:
Developerzy naszej aplikacji wciąż dodawali kolejne migracje:
I wtedy developerzy dostają nowe zadanie. Muszą uruchomić aplikację na nowym środowisku dla innego klienta. Z pomocą DevOpsów uruchamiają aplikację na nowym środowisku przy pomocy niezawodnych dotychczas skryptów zawartych w ramach procesu CI/CD.
W kontekście bazy danych plan jest taki, aby utworzyć zupełnie „czystą” schemę bazy danych, czyli po prostu uruchomić migracje od stanu „zerowego”.
Jak się okazuje, nie jest to możliwe:
Jedna z migracji rzuciła nam błąd. Oczywiście jest to feralna migracja, w której jeden z developerów zaingerował w schemę bazy danych jednego z pluginów. Niefortunnie twórcy plugina zmienili później nazwę tej tabeli. Doctrine, który w trybie domyślnym uruchamia migracje poprzez sortowanie klas migracji alfabetycznie, nie był w stanie samodzielnie zadecydować o tym, w jaki sposób uruchomić migracje w sposób prawidłowy.
Problem
Problemem, który rozważamy, jest fakt, iż doctrine-migrations-bundle uruchamia migracje poprzez alfabetyczne posortowanie nazwy klasy i jej namespace’a.
Na podstawie tła, które przedstawiłem, możemy uprościć nasz problem do następującej postaci:
Z perspektywy aplikacji jest dla nas logiczne, iż chcielibyśmy, aby migracje zostały uruchomione, poprzez sprawdzenie jedynie ostatniego członu namespace’a. A więc w następującej kolejności:
Z perspektywy doctrine/migrations-bundle jest to nielogiczne, ponieważ nazwy te nie są posortowane alfabetycznie.
Rozwiązanie
W obliczu tego problemu najprostszym rozwiązaniem jest użycie następującej implementacji.
Doctrine używa własnej implementacji odpowiedzialnej za porównywanie wersji w algorytmie sortowania:
Możemy zatem stworzyć własną implementację Comparatora:
A następnie dokonać prostej konfiguracji:
Po odświeżeniu cache’u migracje zostaną uruchomione w pożądany sposób:
Inne problemy i rozwiązania
W tym artykule przedstawiłem jedynie wierzchołek góry lodowej. W rzeczywistości problemy z kolejnością uruchamiania migracji bazodanowych mogą być zupełnie inne. Wszystko zależy od pożądanego rezultatu i tego, w jaki sposób w aplikacji były dodawane kolejne migracje.
Częstą sytuacją, z którą możemy się spotkać, jest to, iż migracje z jednego modułu będziemy chcieli uruchomić przed migracjami z innego. Czyli zamiast:
Chcemy uzyskać następujący rezultat:
Oczywiście podobnie jak w proponowanym przeze mnie rozwiązaniu możemy pisać własną implementację Comparatora. Jednak po co to robić, skoro ktoś już to zrobił za nas? Istnieje świetna implementacja SyliusLabs/DoctrineMigrationsExtraBundle, która pozwoli nam na utworzenie hierarchii między migracjami modułów.
Podsumowanie
Mam nadzieję, iż artykuł ci się podobał i zwróciłem Twoją uwagę na problem kolejności uruchamiania migracji. W różnych sytuacjach i kontekstach rozwiązanie tego problemu może się wydawać nieoczywiste. Warto zapoznać się z dostępnymi możliwościami customizacji zachowania doctrine/migrations-bundle.
Czy zdarzyły Ci się jakieś interesujące problemy z migracjami bazodanowymi? A o ile tak, to w jaki sposób udało Ci się je rozwiązać? Daj znać w komentarzu!