Liquibase – zarządzanie zmianami bazy danych
Przeważająca większość aplikacji webowych wykorzystuje w większym lub mniejszym stopniu gdzieś „pod spodem” bazy danych. Przy rozwoju takich aplikacji, często spotykanym problemem jest zarządzanie zmianami w strukturze bazy danych (ciągle dochodzą nowe kolumny, zmieniane są typu kolumn itd.). Bardzo przydatne może okazać się wtedy narzędzie Liquibase, na które natrafiłem ostatnio przypadkiem. W bardzo prosty sposób można za jego pomocą zapanować nad wszelkimi zmianami, które zostaną dokonane po wdrożeniu już u klienta aplikacji. Liquibase w swoisty sposób zarządza wersjami struktury bazy danych. Tworząc schemat, przy użyciu tego narzędzia, tworzone są dodatkowo dwie tabele databasechangelog oraz databasechangeloglock. W tabeli databasechangelog znajdują wpisy opisujące wszystkie zmiany jakie zostały już wgrane do bazy danych, tak aby zostały one wykonane tylko raz, natomiast tabela databasechangeloglock stosowana jest w celu uniknięcia kłopotów w momencie, gdy kilka maszyn próbuje zaktualizować tą samą bazę danych.
Konfiguracja i uruchomienie Liquibase
Ale może zacznijmy od początku. Do uruchomienia i skonfigurowania Liquibase użyłem wtyczki do Maven’a. Poniżej konfiguracja:
<plugin> <groupId>org.liquibase</groupId> <artifactId>liquibase-maven-plugin</artifactId> <version>2.0.1</version> <configuration> <changeLogFile>src/main/resources/db-changelog.xml</changeLogFile> <propertyFile>src/main/resources/jdbc.properties</propertyFile> </configuration> </plugin>
W sekcji konfiguracji wskazujemy na dwa pliki. W pliku jdbc.properties znajduje się konfiguracja podłączenia do bazy danych:
driver=org.postgresql.Driver url=jdbc:postgresql://localhost:5432/testBase username=test password=testpass
Jak widać są to standardowe właściwości potrzebne do nawiązania połączenia przez JDBC, w moim przypadku jest to baza PostgreSQL. Liquibase wspiera wiele innych baz danych, listę obsługiwanych baz można znaleźć tutaj. W kolejnym pliku db-changelog.xml definiowane są wszystkie zmiany jakie mają być dokonane na bazie danych. Każda zmiana jest zdefiniowana w sekcjach changeset, które identyfikowane są po id.
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd"> <changeSet id="schemat" author="andrzej.holowko"> <createTable tableName="customers"> <column autoIncrement="true" name="id" type="bigserial"> <constraints nullable="false" primaryKey="true" primaryKeyName="customers_pk"/> </column> <column name="name" type="VARCHAR(200)"> <constraints nullable="false"/> </column> <column name="address" type="VARCHAR(200)"/> <column name="email" type="VARCHAR(50)"/> <column name="phone" type="VARCHAR(50)"/> </createTable> <createTable tableName="orders"> <column autoIncrement="true" name="id" type="bigserial"> <constraints nullable="false" primaryKey="true" primaryKeyName="orders_pk"/> </column> <column name="description" type="VARCHAR(500)"/> <column name="amount" type="int4"> <constraints nullable="false"/> </column> <column name="users_id" type="int8"> <constraints nullable="false"/> </column> <column name="customers_id" type="int8"> <constraints nullable="false"/> </column> <column name="duedate" type="DATE"/> </createTable> <createTable tableName="users"> <column autoIncrement="true" name="id" type="bigserial"> <constraints nullable="false" primaryKey="true" primaryKeyName="users_pk"/> </column> <column name="username" type="VARCHAR(200)"> <constraints nullable="false" unique="true"/> </column> <column name="password" type="VARCHAR(200)"> <constraints nullable="false"/> </column> <column name="firstname" type="VARCHAR(300)"/> <column name="lastname" type="VARCHAR(300)"/> </createTable> <addForeignKeyConstraint baseColumnNames="customers_id" baseTableName="orders" baseTableSchemaName="public" constraintName="customers_orders_fk" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="customers" referencedTableSchemaName="public" referencesUniqueColumn="false"/> <addForeignKeyConstraint baseColumnNames="users_id" baseTableName="orders" baseTableSchemaName="public" constraintName="users_orders_fk" deferrable="false" initiallyDeferred="false" onDelete="NO ACTION" onUpdate="NO ACTION" referencedColumnNames="id" referencedTableName="users" referencedTableSchemaName="public" referencesUniqueColumn="false"/> </changeSet> <changeSet id="uzytkownicy" author="andrzej.holowko"> <insert tableName="users"> <column name="username" value="admin"/> <column name="password" value="21232f297a57a5a743894a0e4a801fc3"/> <column name="firstname" value="Administrator"/> </insert> </changeSet> </databaseChangeLog>
Po wczytaniu takiego pliku przez Liquibase w bazie zostaną stworzone trzy tabele, powiązane ze sobą relacjami wiele do jeden (np. każdy klient może mieć wiele zamówień, podobnie jak użytkownik). To w ramach pierwszej zmiany, natomiast po wykonaniu drugiej zmiany stworzony zostanie wpis do tabeli z użytkownikami. Struktura tego xml’a jest dość prosta i samoopisująca się. Po opis możliwości Liquibase zapraszam do dokumentacji.
Aby wczytać te zmiany należy wykonać polecenie mvn liquibase:update. Można oczywiście korzystać z tego narzędzia nie tylko przy użyciu Mavena.
Jak zacząć gdy posiadamy już bazę danych?
Przydatna jest funkcja wygenerowania pliku db-changelog.xml na podstawie istniejącej już bazy danych. Zazwyczaj jest tak przecież że pracujemy już na jakiejś bazie. W bardzo łatwy sposób można uzyskać zrzut jej struktury do definicji rozumianej przez liquibase. Wystarczy wykonać powniższy skrypt (uprzednio odpowiednio modyfikując ścieżki do plików bibliotek z liquibase oraz sterownika JDBC do naszej bazy)
set JARFILE="%USERPROFILE%\.m2\repository\org\liquibase\liquibase-core\2.0.1\liquibase-core-2.0.1.jar" set DRIVER="%USERPROFILE%\.m2\repository\postgresql\postgresql\9.0-801.jdbc4\postgresql-9.0-801.jdbc4.jar" java -jar %JARFILE% --driver=org.postgresql.Driver --classpath=%DRIVER% --changeLogFile=db.changelog.xml --url="jdbc:postgresql://localhost:5432/testBase" --username=test --password=testpass generateChangeLog
Dzięki temu że narzędzie Liquibase zostało napisane w javie, wykorzystując jego API, można uruchamiać aktualizację przy np. wstawaniu po raz pierwszy aplikacji na serwerze. Przy takim rozwiązaniu będziemy pewni że nasza aplikacja uruchomi się poprawnie, gdyż wszystkie zmiany dotyczące bazy danych zostaną rozwiązane przez Liquibase. Zachęcam do zapoznania się z tym narzędziem :). Mnie ułatwiło ono pracę i to znacznie 🙂
No nareszcie jakiś wpis. Już myślałem, że zapomniałeś o swoich wiernych czytelnikach 🙂
Narzędzie ciekawe, jednak odnoszę wrażenie, że przydatne tylko przy małych projektach i mało skomplikowanych strukturach bazy. U „dużych” klientów raczej nikt nie pozwoli na aktualizację schematu bazy z automatu przez aplikację, więc tak czy siak trzeba tworzyć osobne patch’e.
Nie zagłębiałem się zbytnio w dokumentację, ale może będziesz wiedział, czy można plik db-changelog.xml rozbić na wiele mniejszych plików? Jeśli zmian w bazie będzie dużo, jeden plik może urosnąć do kolosalnych rozmiarów. Czy istnieje możliwość wywoływania w takim pliku zdefiniowanych osobno skryptów SQL? Jak wygląda sprawa np. tworzenia procedur na bazie?