Flyway – prosta migracja bazy danych w Spring

Filip Raszka

W zależności od charakteru projektu i jego wymagań nie zawsze warto polegać na całkowicie zautomatyzowanych narzędziach do generowania struktury bazy danych takich jak np. Hibernate. Czasami niezbędna jest większa kontrola nie tylko nad kształtem bazy danych, ale także nad jej późniejszym wersjonowaniem. Szczególnie ważne jest to w przypadku bardziej złożonych baz danych w projektach, w których kładzie się nacisk na niezawodność. Co wtedy wybrać? Jedną z propozycji jest Flyway. Nie tylko pomaga organizować i zarządzać migracjami SQL, ale daje te szerokie możliwości ich konfiguracji.

Koncepcja Flyway’a

Flyway to narzędzie do migracji baz danych typu open source, które można skonfigurować w aplikacji korzystającej z bazy danych. Obsługuje siedem podstawowych poleceń: MigrateCleanInfoValidateUndoBaseline oraz Repair. Po uruchomieniu w bazie danych powstaje nowa tabela o nazwie flyway_schema_history. Przechowywane są w niej wszystkie dane dotyczące uruchomionych migracji – w tym m.in. wersje, czas, nazwy skryptów, ich sumy kontrolne, status migracji. Flyway używa ich, wraz z rzeczywistymi skryptami znajdującymi się w określonej lokalizacji w projekcie, do określenia ogólnego stanu bazy danych.

Podczas pracy z Flyway’em po prostu dodajesz tworzone przez siebie migracje do określonej lokalizacji, odpowiednio je nazywając i wersjonując, a następnie uruchamiając polecenie migrate (z wiersza poleceń lub podczas uruchamiania aplikacji). Szczegółowo omówię te polecenia w dalszej części artykułu.

Migracje z Flyway

Domyślną lokalizacją skryptów jest classpath:db/migration, tak więc w projekcie Spring zazwyczaj umieszczamy pliki SQL w katalogu resources/db/migration. Flyway obsługuje kilka rodzajów migracji, rozpoznając ich określone typy na podstawie konwencji nazw plików.

Standardowa migracja wraz z wersjonowaniem

Takie migracje mają następującą konwencję nazewnictwa V<Version>__<name>.sql, np. V1.0.1__create_user_table.sql. Każdy z takich plików jest uruchamiany tylko raz i zazwyczaj zawieraj SQL tworzący bądź modyfikujący tabele i inne obiekty. Przy migracji Flyway porównuje zawartość flyway_schema_history ze skryptami w Twoim projekcie, aby określić, które z nich powinny zostać uruchomione. Kolejność uruchamiania skryptów zależy od numeracji wersji zawartej jako część nazwy skryptu.

Na przykład, jeśli masz następujące skrypty…

  • V1.0.1__create_user_table.sql
  • V1.0.2__adjust_user_table.sql
  • V1.0.3__create_task_table.sql

…to migracje będą uruchamiane od góry do dołu zgodnie z numeracją. Natomiast jeśli przed uruchomieniem migracji flyway_schema_history zawiera już informacje o pomyślnym uruchomieniu migracji V1.0.1__create_user_table.sql w przeszłości, uruchomione zostaną tylko dwie następne migracje.

Jak wygląda przykładowa zawartość takiego pliku SQL wersjonowanej migracji? Spójrz:

CREATE TABLE user
(
    id        BIGINT(20)   NOT NULL AUTO_INCREMENT,
    email     VARCHAR(100) NOT NULL,
    password  VARCHAR(255) NOT NULL,
    join_date TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP,

    PRIMARY KEY (id),
    UNIQUE KEY UK_user_email (email)
);

Migracje te powinny być uruchomione tylko raz, aby zapewnić spójność bazy danych. Flyway sprawdza, czy sumy kontrolne już uruchomionych migracji (przechowywane w tabeli flyway_schema_history) są zgodne z aktualnymi wersjami skryptów. Jeśli edytujesz skrypt dla już uruchomionej migracji, Flyway wykryje niespójność. Nie pozwali więc na uruchomienie migracji, dopóki problem nie zostanie rozwiązany. Zobacz:

Rozwiązaniem jest albo przywrócenie zmian w skrypcie, albo – jeśli masz pewność, że rzeczywisty stan bazy danych odpowiada zmodyfikowanemu skryptowi – uruchomienie polecenia flyway repair, które wyrówna sumy kontrolne.

Powtarzalna migracja

Tego typu migracje charakteryzują się nazewnictwem R__<name>.sql, np. R__fill_task_table.sql. Nie mają one wersjonowania, ponieważ domyślnie są zawsze uruchamiane jako ostatnie. Są one również uruchamiane za każdym razem, gdy zmienia się ich suma kontrolna. Oznacza to, że możesz dowolnie edytować takie skrypty i zostaną one po prostu uruchomione ponownie. Zazwyczaj używa się powtarzalnych migracji do populowania danych.

Jak może wyglądać zawartość takiej migracji? Zobacz:

DELETE FROM task where name LIKE 'Test name%';
INSERT INTO task(name, description) VALUES ('Test name1', 'Test description1');
INSERT INTO task(name, description) VALUES ('Test name2', 'Test description2');

Migracja cofająca zmiany

Jeśli chcesz cofnąć jakieś zmiany i użyć polecenia flyway undo, musisz dodać migracje typu undo. Te specjalne migracje muszą być nazwane dokładnie tak, jak odpowiadające im migracje wersjonowane. Wyjątek stanowi zmiana początkowej litery, gdzie musisz zmienić V na U, np. U1.0.1__create_user_table.sql. Zawartość migracji tego typu musi przywracać zmiany dokonane w odpowiadającej jej migracji wersjonowanej.

Jak może wyglądać migracja typu undo? Spójrz:

DROP TABLE user;

Typ migracji Undo nie jest wspierany w wersji community.

Bazowe migracje

W przypadku tych migracji konwencja nazewnictwa przedstawia sie następująco: B<Version>__<name>.sql, np. B2.0.0__create_basic_tables.sql. Baseline to specjalny rodzaj migracji wersjonowanej, który służy jako agregacja poprzednich skryptów do określonej wersji. Ta opcja pozwala Ci usprawnić wiele migracji, które zostały dodane i były modyfikowane podczas procesu rozwoju w jedną uproszczoną.

Mając następujące skrypty…

  • V1__create_user_table.sql
  • V2__adjust_user_table.sql
  • V3__create_task_table.sql
  • B3__create_basic_tables.sql

…po włączeniu polecenia migrate na świeżej bazie danych zostanie uruchomiona tylko migracja bazowa.

Flyway komendy i wtyczka do Mavena

Przyjrzyjmy się teraz samym poleceniom Flyway.

Zazwyczaj wykonuje się polecenia Flyway w terminalu, używając wiersza poleceń – do czego dostępne są dedykowane narzędzie. Dobrą alternatywą może być jednak wtyczka Flyway do Mavena.

    <plugin>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-maven-plugin</artifactId>
        <configuration>
            <url>jdbc:mysql://localhost:3306/database</url>
            <user>username</user>
            <password>password</password>
            <locations>
                <location>classpath:db/migration</location>
            </locations>
        </configuration>
    </plugin>

Mając poprawną konfigurację bazy danych i zbudowany projekt, możesz wywoływać swoje polecenia w następujący sposób: mvn flyway:<command>, np. mvn flyway:migrate.

Komendy Flyway

migrate

To polecenie spowoduje, że Flyway porówna zawartość tabeli flyway_schema_history z bieżącym stanem Twoich skryptów. Po co? Aby określić, czy sumy kontrolne są zgodne i czy istnieją oczekujące migracje zgodnie z regułami dla określonych typów migracji. Jeśli istnieją migracje do uruchomienia, zostaną one uruchomione zgodnie z numeracją wersji, z powtarzalnymi migracjami jako ostatnimi. Natomiast jeśli baza danych nie zawiera jeszcze tabeli flyway_schema_history, zostanie ona utworzona.

Jak wyglądają przykładowe wyniki dla czystej bazy danych? Spójrz:

info

To polecenie sprawi, że Flyway porówna tabelę flyway_schema_history z bieżącymi skryptami i zwróci status migracji w czytelnym i zrozumiałym formacie. Efekt wykonania tego polecenia to:

Cpo w sytuacji, gdy nie ma żadnych oczekujących migracji? Zobacz poniższy przykład:

Tu natomiast prezentuję przykład, gdy wykonanie jednego ze skryptów zakończy się niepowodzeniem:

repair

To polecenie wyrówna sumy kontrolne skryptów migracji w flyway_schema_history i usunie z niej wszystkie nieudane wiersze migracji. Pamiętać, że nie naprawia to samych skryptów. Tę komendę należy uruchomić, gdy już dokonasz korekty skryptów, aby Flyway odświeżył stan i wyszedł ze stanu niekonsystencji.

Przykład takiej sytuacji? Mamy bazę danych z dwoma poprawnymi migracjami już przeprowadzonymi i nową migracją C w toku. W skrypcie C występuje błąd składni sql. Co zrobić?

  • Uruchamiamy polecenie migrate, które kończy się niepowodzeniem. Otrzymujemy informację o przyczynie niepowodzenia, a wiersz w tabeli flyway_schema_history ma oznaczenie „zakończony niepowodzeniem”. Mechanizm Flyway jest teraz zblokowany i nie zezwala na dalsze migracje, ponieważ nie może zagwarantować, że baza danych jest w spójnym stanie.
  • Ręcznie naprawiamy migrację C i usuwamy potencjalne efekty uboczne z bazy danych.
  • Uruchamiamy polecenie repair, które usuwa uszkodzony wiersz.
  • Teraz ponownie uruchamiamy polecenie migrate, które powinno tym razem zakończyć się pomyślnie.

Spójrz na przykładowy wynik komendy repair:

clean

To polecenie czyści nam cały schemat bazy danych, usuwając wszystkie tabele, włączając w to flyway_schema_history. Polecenie to jest domyślnie wyłączone, aczkolwiek w środowisku testowym, gdzie może być przydatne, możesz ręcznie zmienić ustawienie cleanDisabled na false, aby je włączyć.

validate

Ta komenda wykona analizę i walidacje tak jak przy poleceniu migrate (sumy kontrolne, nazwy), ale bez faktycznego uruchamiania migracji. Jest to przydatne do wykrywania wszelkich nieoczekiwanych zmian w skryptach, które mogłyby prowadzić do nieprawidłowego odtworzenia bazy danych w innym środowisku.

baseline

Komenda ta, przeznaczona do uruchamiania na istniejącym schemacie, pobierze wartość baselineVersion i oznaczy nią bazę danych, tworząc tabelę flyway_schema_history wskazującą na konkretną wersję. Migracje wersji poniżej wersji bazowej nie będą uruchamiać się na takiej bazie danych.

undo

To polecenie umożliwia cofnięcie ostatnio zastosowanej migracji wersjonowanej. Do wykonania tej komendy niezbędna jest obecności cofającej migracji (typu undo) odpowiadającej migracji wersjonowanej. Komenda ta nie ma wsparcia w wersji community.

Konfiguracja Flyway’a

Flyway ma różne możliwości konfiguracji, które mogą zmienić jego działanie. Co najważniejsze, należy skonfigurować źródło danych i ustawić lokalizację skryptów migracyjnych. Poniżej znajdują się możliwe opcje dla springowego projektu, uporządkowane według pierwszeństwa.

  • Możesz przekazać wartości parametrów podczas uruchamiania projektu: mvn -Dflyway.user=username -Dflyway.password=password -Dflyway.url=jdbc:mysql://localhost:3306/database.
  • Możesz też stworzyć osobny plik konfiguracyjny flyway.conf znajdujący się w głównym katalogu i zawierający następujące parametry:
flyway.user=username
flyway.password=password
flyway.url=jdbc:mysql://localhost:3306/database
  • Możesz ustawić także Mavena, zarówno w pliku pom, jak i w profilach zawartych w plikach yaml.
    <properties>
        <flyway.user>username</flyway.user>
        <flyway.password>password</flyway.password>
        <flyway.url>jdbc:mysql://localhost:3306/database</flyway.url>
        ...
    </properties>

Będziemy używać profili yaml, ponieważ jest to jedno z wygodniejszych rozwiązań w takim projekcie. Domyślnie Flyway jest włączony oraz używa podstawowego źródła danych naszego projektu. Domyślną lokalizacją skryptów jest: classpath:db/migration. To oznacza, że jedyne, co musisz skonfigurować, to źródło danych dla projektu.

  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: ${MYSQL_URL}
    username: ${MYSQL_USERNAME}
    password: ${MYSQL_PASSWORD}

Dodanie Flyway’a do projektu springowego

Oprócz konfiguracji, aby Flyway działał w projekcie Spring, musisz dodać zależność…

  <dependency>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-core</artifactId>
    </dependency>

…oraz możliwie konkretną konfigurację typu bazy danych, w zależności od tego, z czego korzystasz:

    <dependency>
        <groupId>org.flywaydb</groupId>
        <artifactId>flyway-mysql</artifactId>
    </dependency>

Masz skonfigurowane źródło danych? To powinno wystarczyć, aby Flyway działał i automatycznie migrował bazę danych podczas uruchamiania aplikacji:

2023-07-10T15:00:41.705+02:00  INFO 15705 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 4 migrations (execution time 00:00.013s)
2023-07-10T15:00:41.719+02:00  INFO 15705 --- [           main] o.f.c.i.s.JdbcTableSchemaHistory         : Creating Schema History table `jblog_common`.`flyway_schema_history` ...
2023-07-10T15:00:41.883+02:00  INFO 15705 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema `jblog_common`: << Empty Schema >>
2023-07-10T15:00:41.888+02:00  INFO 15705 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `jblog_common` to version "1.0.1 - create user table"
2023-07-10T15:00:42.007+02:00  WARN 15705 --- [           main] o.f.c.i.s.DefaultSqlScriptExecutor       : DB: Integer display width is deprecated and will be removed in a future release. (SQL State: HY000 - Error Code: 1681)
2023-07-10T15:00:42.078+02:00  INFO 15705 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `jblog_common` to version "1.0.2 - create task table"
2023-07-10T15:00:42.182+02:00  WARN 15705 --- [           main] o.f.c.i.s.DefaultSqlScriptExecutor       : DB: Integer display width is deprecated and will be removed in a future release. (SQL State: HY000 - Error Code: 1681)
2023-07-10T15:00:42.237+02:00  INFO 15705 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `jblog_common` with repeatable migration "add java task"
2023-07-10T15:00:42.306+02:00  INFO 15705 --- [           main] o.f.core.internal.command.DbMigrate      : Migrating schema `jblog_common` with repeatable migration "fill task table"
2023-07-10T15:00:42.360+02:00  INFO 15705 --- [           main] o.f.core.internal.command.DbMigrate      : Successfully applied 4 migrations to schema `jblog_common`, now at version v1.0.2 (execution time 00:00.481s)

Migracja z wykorzystaniem Javy

Możesz również stworzyć własne migracje z wykorzystaniem Javy. Wszystko, co musisz zrobić, to przestrzegać konwencji nazewnictwa dla klasy org.flywaydb.core.api.migration.BaseJavaMigration, a także zadbać o umieszczenie plików z rozszerzeniem .java w odpowiedniej lokalizacji (domyślnie: db/migration).

package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;

import java.sql.Statement;

public class R__add_java_task extends BaseJavaMigration {

    @Override
    public void migrate(Context context) throws Exception {
        try (Statement statement = context.getConnection().createStatement()) {
            statement.execute("INSERT INTO task(name, description) " +
                    "VALUES ('Java-migrated task name', 'Java-migrated task desc');");
        }
    }
}

Jak widzisz, dodana migracja pojawia się w informacji wygenerowanej przez Flyway.

Użycie beana Flyway

Chociaż zwykle nie jest to konieczne, możesz również uzyskać dostęp do beana Flyway z kontekstu springowego i wykonywać na nim operacje:

private final Flyway flyway;
public void flyway() {
        MigrationInfoService infoService = flyway.info();
    }

Wnioski

Flyway to wszechstronne, a zarazem proste narzędzie do migracji, które bardzo dobrze współgra z projektami Spring Boot. Daje ono dużą władzę i kontrolę nad zarządzaniem bazą danych. Co więcej, co pokazałem, jego konfiguracja jest bardzo prosta. Dlaczego by więc nie spróbować?

Bibliografia

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami