Trudno sobie obecnie wyobrazić poważny system bez dostępu do bazy danych, która przechowywałaby dane zbierane i przetwarzane przez aplikację. Niezależnie od wykorzystywanego systemu zarządzania bazą danych, niezbędne jest stworzenie schematu, a dość często, w miarę rozwoju aplikacji potrzebna jest jego aktualizacja. Oczywiście nic nie stoi na przeszkodzie, żeby tworzyć odpowiednie skrypty SQL i wykonywać je w razie potrzeby. Słabość takiego podejścia tkwi w ułomności natury ludzkiej, która może doprowadzić do ponownego wykonania tych samych skryptów lub niewykonania ich w ogóle. Automatyzacja tego procesu eliminuje te problemy. Jednym z narzędzi pozwalających na stworzenie mechanizmu migracji bazy danych jest Liquibase.

Zacznijmy od pobrania biblioteki Liquibase: https://github.com/liquibase/liquibase/releases/download/liquibase-parent-3.5.3/liquibase-3.5.3-bin.zip Archiwum zawiera skrypty dla środowiska Windows i Mac/UNIX niezbędne do zaaplikowania zmian w bazie danych.

Aby zdefiniować kroki migracji bazy danych, tworzymy plik db.changelog.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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-3.1.xsd">
	<!-- każdy krok migracji musi zawierać się w znacznikach changeSet -->
	<!-- changeSet musi posiadać unikalny identyfikator (id) i autora (author) -->
	<changeSet id="1" author="szymon">
		<!-- stworzenie tabeli o nazwie "person" -->
		<createTable tableName="person">
			<!-- zdefiniowanie kolumny o nazwie "id" typu całkowitego (int) z autoinkrementacją -->
			<column name="id" type="int" autoIncrement="true">
				<!-- kolumna będzie kluczem głównym z niepustą zawartością -->
				<constraints primaryKey="true" nullable="false"/>
			</column>
			<!-- zdefiniowanie kolumny o nazwie "first_name" typu znakowego (varchar) o długości 256 znaków -->
			<column name="first_name" type="varchar(256)">
				<!-- kolumna z niepustą zawartością -->
				<constraints nullable="false"/>
			</column>
			<!-- zdefiniowanie kolumny o nazwie "last_name" typu znakowego (varchar) o długości 256 znaków -->
			<column name="last_name" type="varchar(256)">
				<!-- kolumna z niepustą zawartością -->
				<constraints nullable="false"/>
			</column>
		</createTable>
	</changeSet>
</databaseChangeLog>

Kolejny krok to wykonanie skryptu liquibase:

./liquibase --driver=com.mysql.jdbc.Driver --classpath=PATH_TO_CONNECTOR/mysql-connector-java-5.1.41-bin.jar --changeLogFile=PATH_TO_CHANGELOG/db.changelog.xml --url="jdbc:mysql://localhost:3306/test" --username="root" --password="root" update

W przypadku, jeśli chcielibyśmy wyczyścić bazę danych, żeby np. wykonać migrację jeszcze raz (od początku), można zastąpić opcję update przez dropAll. Jeśli zależy nam na wycofaniu ostatnio zaaplikowanych zmian można użyć opcji rollbackCount (z liczbą zmian do wycofania). Oczywiście skrypt ma dużo większe możliwości ale po opis ich odsyłam do dokumentacji (http://www.liquibase.org/documentation/command_line.html).

Alternatywnym sposobem wykonania migracji jest użycie narzędzie Maven. Jest to o tyle wygodne, że może być sprzężone z procesem budowania, dzięki czemu baza danych zaktualizuje się wraz z kompilacją i uruchomieniem systemu, co zapobiega przeoczeniu wykonania jej (przynajmniej na maszynie deweloperskiej). Zacząć należy od dodania zależności w pliku pom.xml w poddrzewie project/dependencies:

<dependencies>
	(...)
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<version>5.1.41</version>
	</dependency>
	(...)
</dependencies>

Następnie dodajemy konfigurację wtyczki w sekcji project/build/plugins:

<plugins>
	(...)
	<plugin>
		<groupId>org.liquibase</groupId>
		<artifactId>liquibase-maven-plugin</artifactId>
		<version>3.5.3</version>
		<configuration>
			<propertyFile>src/main/resources/liquibase/liquibase.properties</propertyFile>
		</configuration>
		<executions>
			<execution>
				<phase>process-resources</phase>
				<goals>
					<goal>update</goal>
				</goals>
			</execution>
		</executions>
	</plugin>
	(...)
</plugins>

W kolejnym kroku dodajemy niezbędny plik z ustawieniami dla wtyczki Liquibase (src/main/resources/liquibase/liquibase.properties):

changeLogFile=src/main/resources/liquibase/db.changelog.xml
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
username=root
password=root

Ostatecznie w miejscu, gdzie znajduje się pom.xml wykonujemy:

mvn liquibase:update

Powyższe rozwiązanie jest prawie idealne. Do pełni automatyki brakuje sprawdzenia podczas uruchomienia aplikacji, czy na pewno zostały wykonane wszystkie migracje (np. na serwerze produkcyjnym na którym kod nie był budowany za pomocą narzędzia Maven, tylko uruchomiony z gotowej paczki). Do wykonania tego wystarczą nam starndardowe mechanizmy z Javy EE i EJB.

Należy zacząć od dodania niezbędnych zależności w pliku pom.xml:

<dependencies>
	(...)
	<dependency>
		<groupId>javax</groupId>
		<artifactId>javaee-api</artifactId>
		<version>7.0</version>
	</dependency>
	<dependency>
		<groupId>org.liquibase</groupId>
		<artifactId>liquibase-core</artifactId>
		<version>3.5.3</version>
	</dependency>
	(...)
</dependencies>

Po czym nie pozostaje nic innego jak implementacja serwisu uruchamianego przy starcie serwera:

import java.sql.Connection;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.sql.DataSource;

import liquibase.Liquibase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;

@Singleton
@Startup
@TransactionManagement(value = TransactionManagementType.BEAN)
public class SetupBean {

	private static final String CHANGE_LOG_FILE = "liquibase/db.changelog.xml";
	private static final String CONTEXT = "test";

	private static final Logger logger = Logger.getLogger(SetupBean.class.getName());

	@Resource(lookup = "java:/TestDS")
	private DataSource dataSource;

	@PostConstruct
	public void install() {
		try {
			Liquibase liquibase = getLiquibase();
			liquibase.update(CONTEXT);
		}
		catch (LiquibaseException | SQLException e) {
			logger.log(Level.SEVERE, e.getMessage(), e);
		}
	}

	private Liquibase getLiquibase() throws LiquibaseException, SQLException {
		ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
		Connection connection = dataSource.getConnection();
		JdbcConnection jdbcConnection = new JdbcConnection(connection);
		DatabaseFactory databaseFactory = DatabaseFactory.getInstance();
		Database database = databaseFactory.findCorrectDatabaseImplementation(jdbcConnection);
		Liquibase liquibase = new Liquibase(CHANGE_LOG_FILE, resourceAccessor, database);
		return liquibase;
	}

}

Przykładowy projekt: