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 🙂