Flyway – prosta migracja bazy danych w Spring

Filip Raszka

Flyway to narzędzie do migracji baz danych typu open source, które można skonfigurować w aplikacji korzystającej z bazy danych. 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 choćby Hibernate. Czasami chcemy mieć większą kontrolę – nie tylko nad kształtem bazy danych, ale także nad jej późniejszym wersjonowaniem. Jest to szczególnie ważne w przypadku bardziej złożonych baz danych w projektach, w których kładzie się nacisk na niezawodność. Flyway pomaga nam organizować i zarządzać naszymi własnymi migracjami SQL, z szerokimi możliwościami ich konfiguracji.

Koncepcja Flyway’a

Flyway obsługuje siedem podstawowych poleceń: MigrateCleanInfoValidateUndoBaseline oraz Repair. Po uruchomieniu w naszej bazie danych zostaje utworzona nowa tabela o nazwie flyway_schema_history, w której przechowywane są wszystkie dane dotyczące uruchomionych migracji, w tym wersje, czas, nazwy skryptów, ich sumy kontrolne, status migracji i inne. Flyway używa tych danych, 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 dodajemy tworzone przez nas migracje do określonej lokalizacji, odpowiednio je nazywając i wersjonując, a następnie uruchamiając polecenie migrate, czy to z wiersza poleceń, czy 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 naszym 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 posiadają następującą konwencję nazewnictwa V<Version>__<name>.sql, na przykład 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 naszym 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 mamy 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

Migracje te 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.

Poniżej przykładowa zawartość takiego pliku SQL wersjonowanej migracji:

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. Aby to zapewnić, flyway sprawdza, czy sumy kontrolne już uruchomionych migracji (przechowywane w tabeli flyway_schema_history) są zgodne z aktualnymi wersjami skryptów. Jeśli edytujemy skrypt dla już uruchomionej migracji, flyway wykryje niespójność, nie pozwalając nam na uruchomienie migracji, dopóki problem nie zostanie rozwiązany:

Rozwiązaniem jest albo przywrócenie zmian w skrypcie, albo – jeśli jesteśmy pewni, ż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ę takim nazewnictwem R__<name>.sql, na przykład R__fill_task_table.sql. Nie posiadają 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żemy dowolnie edytować takie skrypty i zostaną one po prostu uruchomione ponownie. Zazwyczaj używamy powtarzalnych migracji do populowania danych.

Przykład tego, jak może wyglądać zawartość takiej migracji:

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 chcemy cofnąć jakieś zmiany i użyć polecenia flyway undo, musimy dodać migracje typu undo. Te specjalne migracje muszą być nazwane dokładnie tak, jak odpowiadające im migracje wersjonowane, z wyjątkiem zmiany początkowej litery. Gdzie musimy zmienić V na U, na przykład U1.0.1__create_user_table.sql. Zawartość migracji tego typu musi przywracać zmiany dokonane w odpowiadającej jej migracji wersjonowanej.

Przykład jak może wyglądać migracja typu undo

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, na przykład B2.0.0__create_basic_tables.sql. Migracje typu baseline są specjalnym rodzajem migracji wersjonowanej, które służą jako agregacja poprzednich skryptów do określonej wersji. Ta opcja pozwala nam usprawnić wiele migracji, które dodawaliśmy i modyfikowaliśmy 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

Gdy uruchomimy polecenie migrate na świeżej bazie danych, wówczas zostanie uruchomiona tylko migracja bazowa.

Flyway komendy i wtyczka do maven’a

Przyjrzyjmy się teraz samym poleceniom flyway.

Zazwyczaj wykonujemy polecenia flyway w terminalu, używając wiersza poleceń. Do czego mamy dostępne dedykowane narzędzie, jednakże dobrą alternatywą może być wtyczka flyway do maven’a.

    <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żemy wywoływać nasze polecenia w następujący sposób: mvn flyway:<command>, na przykład mvn flyway:migrate.

Komendy Flyway

migrate

To polecenie spowoduje, że flyway porówna zawartość tabeli flyway_schema_history z bieżącym stanem naszych skryptów, 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.

Przykładowe wyniki dla czystej bazy danych:

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:

Przykład w sytuacji, gdy nie ma żadnych oczekujących migracji:

Poniżej natomiast 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. Należy natomiast pamiętać, że nie naprawia to samych skryptów. Tę komendę należy uruchomić, gdy już sami dokonamy korekty skryptów, aby flyway odświeżył stan i wyszedł ze stanu niekonsystencji.

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

  • Uruchamiamy polecenie migrate, które kończy się niepowodzeniem. Otrzymujemy informacje o przyczynie niepowodzenia, a wiersz w tabeli flyway_schema_history jest teraz oznaczony jako 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.

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żemy 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 jest do uruchamiania na istniejącym schemacie, pobierze ona 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ą uruchamiane 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 jest wspierana w wersji community.

Konfiguracja Flyway’a

Flyway posiada 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 Spring’owego projektu, uporządkowane według pierwszeństwa:

  • Możemy przekazać wartości parametrów podczas uruchamiania projektu: mvn -Dflyway.user=username -Dflyway.password=password -Dflyway.url=jdbc:mysql://localhost:3306/database
  • Możemy 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żemy ustawić także maven’a, 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. Jako domyślna lokalizacja skryptów użyta jest następująca lokalizacja classpath:db/migration. To oznacza, że jedyne co musimy 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 Spring’owego

Oprócz konfiguracji, aby flyway działał w projekcie Spring, musimy 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 na przykład korzystamy:

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

Gdy mamy skonfigurowane źródło danych, to powinno być wystarczające, 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żemy również stworzyć własne migracje z wykorzystaniem Javy. Wszystko, co musimy zrobić, to przestrzegać konwencji nazewnictwa dla klasy, rozszerzeniu org.flywaydb.core.api.migration.BaseJavaMigration, a także zadbać o umieszczeniu 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 widzimy, dodana migracja pojawia się w info wygenerowanym przez flyway.

Użycie bean’a Flyway

Chociaż zwykle nie jest to konieczne, można również uzyskać do bean’a 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 nam dużą władzę i kontrolę nad zarządzaniem bazą danych. Co więcej, jak widzieliśmy, 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