Datenbankänderungen automatisiert erkennen Liquibase – Versionskontrolle für die Datenbank
Anbieter zum Thema
In der Welt der Datenbanken helfen verschiedene Werkzeuge beim Tracken, Versionieren und Deployen von Änderungen. In diesem Beitrag betrachten wir die Datenbank-Versionkontrolle Liquibase genauer.

Der Weg einer Code-Änderung von der Entwicklung über Test bis hin zum produktiven Deployment ist in den meisten Projekten dank Versionsverwaltung und Continuous-Integration und -Delivery- oder kurz CI/CD-Werkzeugen stets sauber nachvollziehbar. Jederzeit ist klar, welche Version wo ausgeliefert ist.
Was für den Code gilt, sollte natürlich auch für die Datenbankanpassungen gelten. Schließlich kommt nur im Zusammenspiel eine lauffähige Anwendung heraus. Die in der Java-Welt bekanntesten Versionskontrollsysteme für Datenbanken sind Liquibase und Flyway. Beide sind weit verbreitet und bieten ein ähnliches Set an Features.
Als Ausgangspunkt wird hier ein Spring Boot-Projekt verwendet, wie es sich mit dem Initializr oder der Spring Toolsuite (File > New > Spring Starter Project) erstellen lässt. Dabei werden die Dependencies H2 Database, Spring Data JPA, Spring Web, Spring Boot Devtools und Liquibase Migration zugefügt. Liquibase Migration entspricht folgendem Eintrag in pom.xml:
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
Die H2-Datenbank soll hier im Persistenzmodus betrieben werden, nur so lassen sich einige der Liquibase-Features ausprobieren. Der Speicherort wird zusammen mit Username und Password sowie der Aktivierung der h2-Konsole in der Datei application.properties konfiguriert:
spring.datasource.url=jdbc:h2:file:C:/data/demodb
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
ChangeSets
Datenbankänderungen werden von Liquibase in Form von Changesets verwaltet. Anders als beim Konkurrenten Flyway lassen sich diese Datenbankübergreifend in Form von XML oder JSON formulieren. Im Folgenden ist ein Beispiel im XML-Format für das Anlegen einer Tabelle Person wiedergegeben. Die Datei mit Namen create.xml wird im Verzeichnis src/main/resources/db/changelog abgelegt:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog logicalFilePath="db.changelog-master.xml" 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.4.xsd">
<changeSet id="1" author="dirk">
<createTable tableName="person">
<column name="id" type="int">
<constraints primaryKey="true"/>
</column>
<column name="firstname" type="varchar"/>
<column name="lastname" type="varchar"/>
<column name="age" type="int"/>
</createTable>
</changeSet>
</databaseChangeLog>
Damit Liquibase das ChangeSet findet, ist eine weitere Datei erforderlich: db.changelog-master.xml, ebenfalls in src/main/resources/db/changelog anzulegen, listet alle durchzuführenden Changes auf. Hier wird die zuvor angelegte Datei inkludiert:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog logicalFilePath="db.changelog-master.xml" 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.4.xsd">
<include file="create.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
Dummerweise muss auch diese Datei von Liquibase aufgespürt werden, deshalb wird noch ein Eintrag in application.properties benötigt:
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
spring.liquibase.enabled=true
Die Eigenschaft spring.liquibase.enabled=true in application.properties veranlasst Liquibase dazu, beim Start der Anwendung alle nicht bereits eingespielten ChangeSets ausführen. Danach kann die Anwendung gestartet werden. Logausgaben weisen schon darauf hin, dass die Tabelle angelegt wurde.
17:53:08.772 INFO 230924 --- [ restartedMain] liquibase.database : Set default schema name to PUBLIC
17:53:08.953 INFO 230924 --- [ restartedMain] liquibase.lockservice : Changelog-Protokoll erfolgreich gesperrt.
17:53:09.267 INFO 230924 --- [ restartedMain] liquibase.changelog : Creating database history table with name: PUBLIC.DATABASECHANGELOG
17:53:09.271 INFO 230924 --- [ restartedMain] liquibase.changelog : Reading from PUBLIC.DATABASECHANGELOG
Running Changeset: db.changelog-master.xml::1::dirk
17:53:09.366 INFO 230924 --- [ restartedMain] liquibase.changelog : Table person created
17:53:09.367 INFO 230924 --- [ restartedMain] liquibase.changelog : ChangeSet db.changelog-master.xml::1::dirk ran successfully in 5ms
17:53:09.376 INFO 230924 --- [ restartedMain] liquibase.lockservice : Successfully released change log lock
Wer dem nicht traut, kann die h2-Konsole unter http://localhost:8080/h2-console öffnen und sich das Datenbankschema anschauen. Neben der Tabelle Person mit den definierten Spalten finden sich dort zwei weitere Tabellen, die Liquibase automatisch anlegt.
In der Tabelle DATABASECHANGELOG merkt sich Liquibase, welche ChangeSets bereits ausgeführt wurden. Damit wird also verhindert, dass ein bereits eingespieltes ChangeSet erneut angewendet wird. Mithilfe der DATABASECHANGELOGLOCK-Tabelle wird sichergestellt, dass nur eine Liquibase-Instanz läuft und keine konkurrierenden Zugriffe, zum Beispiel von mehreren gleichzeitig startenden Anwendungsinstanzen, stattfinden können. Beide interne Tabellen sollte man selbstverständlich nur lesend verwenden.
Ein ChangeSet zum Einspielen von Testdaten könnte folgendermaßen aussehen:
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog logicalFilePath="db.changelog-master.xml" 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.4.xsd">
<changeSet id="2" author="dirk">
<insert tableName="person">
<column name="id" value="-1"/>
<column name="firstname" value="Max"/>
<column name="lastname" value="Mustermann"/>
<column name="age" value="20"/>
</insert>
</changeSet>
</databaseChangeLog>
An bekannter Stelle (src/main/resources/db/changelog) abgelegt und in db.changelog-master.xml inkludiert, wird es beim erneuten Start der Anwendung ausgeführt. Die im ChangeSet vergebene id muss übrigens eindeutig sein, hier empfiehlt sich eine sinnvolle Namenskonvention (aufsteigende Nummern, Datum mit Uhrzeit oder Ähnliches).
Arbeiten mit dem Plug-in
Im einfachen Beispiel oben wurde Liquibase in die Anwendung integriert. Beim Start werden eventuell vorhandene neue Changelogs ausgeführt und der Stand der Datenbank entspricht dem Stand des zugreifenden Codes.
Wenn man unabhängig vom Starten der App bleiben möchte (zum Beispiel, weil der alte Code mit der neuen DB-Version klarkommt), lässt sich Liquibase auch mithilfe von Maven-Goals ausführen. Dazu ist in pom.xml das Liquibase-Plug-in einzubinden:
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-Plug-in</artifactId>
<version>4.15.0</version>
</dependency>
<Plug-in>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-Plug-in</artifactId>
<version>4.15.0</version>
<configuration>
<propertyFile>src/main/resources/liquibase.properties</propertyFile>
</configuration>
</Plug-in>
In liquibase.properties werden die Verbindungsdaten zur Datenbank (url, username, password) und der Pfad zu db.changelog-master.xml (changeLogFile) für das Plug-in hinterlegt. Das Ausführen der ChangeSets erfolgt dann mit dem Kommando mvn liquibase:update. Das lässt sich natürlich auch von CI/CD-Tools wie zum Beispiel Jenkins starten.
Das Plug-in enthält aber noch eine Menge weiterer nützlicher Werkzeuge. Mit dem Goal liquibase:updateSQL erzeugt man aus dem ChangeSet ein SQL-Skript, das dann vom Datenbankadministrator geprüft und womöglich mit besonderen DBA-Rechten eingespielt werden kann. Auch das generierte SQL-Skript pflegt natürlich die Liquibase-Tabellen, so dass im Endergebnis kein Unterschied zum einfachen update-Goal resultiert.
Ebenfalls sehr hilfreich ist das Goal rollback, mit dem sich ChangeSets rückgängig machen lassen. Für einige Änderungen funktioniert das automatisch, für andere spezifiziert man die Anweisungen explizit mit <rollback>. Weitere Anweisungen ermöglichen das Aufspüren von Unterschieden zwischen zwei Datenbanken (liquibase:diff) oder das Generieren von ganzen ChangeLogs aus einer Datenbank (liquibase:generateChangeLog).
Je nach Kommando können weitere Einträge in liquibase.properties erforderlich sein (zum Beispiel referenceUrl, referenceUsername und referencePassword für den Vergleich zweier Datenbanken). In der Praxis werden das automatische Update und das Maven-Plug-in durchaus nebeneinander eingesetzt (z.B. Startup in Dev, updateSQL in prod). Näheres dazu findet sich im Beitrag „3 Ways to Run Liquibase“.
(ID:48572889)