Datenbankmanagement und Versionierung mit Liquibase

Die Schemaversionierung mit Liquibase erfolgt gemäß den Richtlinien in Versionierung mit Liquibase mit Fokus auf die Integration in Spring Boot und die Nutzung mit Maven. Darüber hinaus werden Informationen zur Nutzung von Liquibase im IsyFact-Kontext bereitgestellt.

Liquibase wird in Spring Boot über die Konfigurationsdateien eingebunden, während das zugehörige Maven-Plugin Schemaänderungen direkt in der Build-Pipeline ausführt.

Durch strukturierte Änderungsprotokolle lassen sich Schema-Anpassungen kontrolliert, nachvollziehbar und automatisiert verwalten. Beim Baseline-Vorgehen bleiben bestehende Datenbanken unverändert und dienen als Basis für zukünftige Änderungen. Die Kombination mit JDBC erlaubt eine flexible Anbindung an verschiedene Datenbanken.

Zudem lässt sich Liquibase nahtlos in CI/CD-Pipelines wie GitLab CI oder GitHub Actions integrieren, um Migrationen und Rollbacks zu automatisieren und so eine sichere Bereitstellung zu gewährleisten.

1. Einrichtung von Liquibase

IT-Systeme nutzen Liquibase direkt sowie als Maven-Plugin. Neben der Einbindung in Maven ist auch eine Konfiguration über Spring Boot notwendig.

1.1. Maven-Konfiguration

Liquibase benötigt JDBC, um mit der Datenbank zu kommunizieren und Änderungen auszuführen. Daher ist neben der Bibliothek liquibase-core, die Kernfunktionen von Liquibase enthält, auch der jeweilige JDBC-Treiber der verwendeten Datenbank notwendig.

Listing 1. Einbindung der Abhängigkeiten im Maven POM
<dependencies>
    <dependency>
        <groupId>org.liquibase</groupId>
        <artifactId>liquibase-core</artifactId>
    </dependency>
    <dependency>
        <!-- JDBC-Treiber der Datenbank -->
    </dependency>
</dependencies>

Neben den Abhängigkeiten ist die Konfiguration des Liquibase Maven Plugin erforderlich.

Listing 2. Konfiguration des Liquibase Maven Plugin als Maven-Properties
<build>
    <plugins>
        <plugin>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-maven-plugin</artifactId>
            <configuration>
                <changeLogFile>src/main/resources/db/changelog/db.changelog-master.xml</changeLogFile>
                <url>${spring.datasource.url}</url>
                <username>${spring.datasource.username}</username>
                <password>${spring.datasource.password}</password>
                <driver>${spring.datasource.driver-class-name}</driver>
            </configuration>
        </plugin>
    </plugins>
</build>

Offizielle Dokumentation von Liquibase: Using JDBC URL in Liquibase

1.2. Spring Boot Konfiguration

Um Liquibase beim Start eines IT-Systems automatisch Schemaänderungen durchführen zu lassen, kann dies in der application.properties konfiguriert werden.

Listing 3. Konfiguration: Automatische Schemaänderungen beim Start
spring.liquibase.enabled=true

Standardmäßig verwendet Liquibase das Root Changelog db/changelog/db.changelog-master.xml. Es dient als zentrale Steuerungsdatei, in der alle Schemaänderungen verwaltet werden. Es enthält entweder direkte Änderungen oder bindet weitere Changelog-Dateien ein, um eine strukturierte Verwaltung zu ermöglichen.

Es ist empfohlen den Pfad immer explizit anzugeben.

Listing 4. Konfiguration: Ablageort des Root Changelogs
spring.liquibase.change-log=classpath:pfad/zur/changelog.xml
Ablage von Credentials der Datenbank

Benutzernamen und Passwörter zum Zugriff auf Datenbanken in Produktion und dazu ähnlichen Umgebungen dürfen nicht in Konfigurationsdateien (application.properties) oder im Maven POM gespeichert werden.

Stattdessen empfiehlt sich die Nutzung von:

  • Umgebungsvariablen ($DB_USER, $DB_PASSWORD),

  • einer zentralen Konfiguration (zum Beispiel Spring Cloud Config), oder

  • Secrets-Management-Tools (zum Beispiel AWS Secrets Manager, HashiCorp Vault, Kubernetes Secrets).

1.3. Rechte-Trennung

Um Sicherheit und Stabilität zu gewährleisten, können DDL- und DML-Berechtigungen strikt getrennt werden:

  • db_ddl führt ausschließlich Schema- und Berechtigungsänderungen aus (DDL).

  • app_dml führt ausschließlich Datenmanipulationen aus (DML).

Dadurch gilt: - Die Anwendung selbst hat niemals DDL-Rechte. - Schlägt ein Deployment fehl, kann durch ein Rollback-Deployment sichergestellt werden, dass Datenkonsistenz erhalten bleibt (z. B. durch Trigger, die Daten zurückschreiben).

Rolle Rechte-Profil Verwendung

db_ddl

CREATE TABLE, ALTER, DROP, CREATE TRIGGER, GRANT (keine DML-Rechte, außer explizit für Migrations-DML im Rahmen von Liquibase)

Liquibase Runner wird ausschließlich in CI/CD-Jobs (z. B. Puppet-Job, Init-Container) eingesetzt, um Schema- und Berechtigungsänderungen durchzuführen.

app_dml

SELECT, INSERT, UPDATE, DELETE auf produktiven Tabellen und Sequenzen (niemals DDL-Rechte)

Runtime-User der Anwendung führt ausschließlich Datenmanipulationen aus.

Offizielle Dokumentation von Liquibase:

2. Erstellung einer Baseline

Die folgenden Schritte beschreiben die Erstellung einer Baseline zur Verwaltung eines bestehenden Schemas.

1. Erstellung der db.changelog-master.xml

Zuerst muss das Root Changelog db.changelog-master.xml erzeugt werden.

Diese Datei dient als Einstiegspunkt für alle Schemaänderungen und stellt sicher, dass die Baseline zuerst und dann alle weiteren Release-Changelogs in der richtigen Reihenfolge angewendet werden.

Bei Verwendung von Spring Boot sollte sich diese Datei im Ordner src/main/resources/db/changelog befinden:

Listing 5. Beispiel für das Root Changelog in XML
<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-4.8.xsd">
    <include file="baseline.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
Listing 6. Beispiel für das Root Changelog in YAML
databaseChangeLog:
  - include:
      file: baseline.yml
      relativeToChangelogFile: true
2. Generierung der baseline.xml
mvn liquibase:generateChangeLog
    -Dliquibase.outputChangeLogFile=src/main/resources/db/changelog/baseline.xml

Nach der Generierung sollte die Datei manuell überprüft und bereinigt werden.

3. Markierung der Baseline

Damit Liquibase die bestehende Datenbank nicht erneut verändert, aber die bereits vorhandenen Strukturen als Referenz speichert, muss die Baseline als ausgeführt markiert werden.

Listing 7. Markierung der Baseline als ausgeführt
mvn liquibase:changelogSync

Dadurch werden alle im Changelog definierten Änderungen als bereits angewendet registriert, ohne tatsächlich Änderungen an der Datenbank vorzunehmen.

Listing 8. Optionale Vorabprüfung ohne Ausführung
mvn liquibase:changelogSyncSQL

Dieser Befehl zeigt die SQL-Befehle an, die Liquibase ausführen würde, ohne sie tatsächlich anzuwenden.

Offizielle Dokumentation von Liquibase:

4. Tabellen DATABASECHANGELOG- und DATABASECHANGELOGLOCK anlegen

Beim ersten Liquibase-Update oder Sync-Vorgang legt Liquibase automatisch zwei Tabellen an:

  • DATABASECHANGELOG: Speichert ausgeführte Changesets.

  • DATABASECHANGELOGLOCK: Sperrt die Datenbank, um gleichzeitige Änderungen zu verhindern.

Falls die Sperre aus einem vorherigen Prozess hängen geblieben ist, kann sie mit folgendem Befehl entfernt werden:

Listing 9. Entfernung der Sperre
mvn liquibase:releaseLocks

Offizielle Dokumentation von Liquibase: Tracking Tables

5. Integration der Baseline in das Versionskontrollsystem

Nachdem die Baseline erstellt wurde, sollten sowohl diese als auch das Root Changelog db.changelog-master.xml in das Versionskontrollsystem aufgenommen werden. Dies gewährleistet eine nachvollziehbare Historie der Datenbankänderungen.

2.1. Gefahr von Schema-Drift und semantische Versionskontrolle mit isy-persistence

Liquibase gewährleistet die Nachvollziehbarkeit aller Datenbankänderungen über die Tabellen DATABASECHANGELOG und DATABASECHANGELOGLOCK. Damit wird sichergestellt, dass jede definierte Änderung genau einmal und in der richtigen Reihenfolge ausgeführt wird.

Liquibase prüft nicht, ob die tatsächliche Datenbankversion mit der vom Anwendungscode erwarteten Release-Version übereinstimmt. Dadurch kann es trotz konsistentem Changelog zum Schema-Drift kommen.

Schema-Drift kann folgende Ursachen haben:

  • manuelle Hotfixes oder Änderungen, ohne dass diese in Liquibase berücksichtigt werden (z. B. ALTER TABLE ohne Changelog),

  • Wiederherstellung eines Snapshots, ohne erneute Anwendung aller Changesets,

  • abgebrochene Deployments, bei denen einzelne Changesets übersprungen wurden.

Solche Abweichungen führen dazu, dass die Anwendung gegen ein nicht kompatibles Schema startet. Dies birgt Risiken wie Laufzeitfehler oder Datenverlust.

2.1.1. Enforcement mit isy-persistence

Um zu verhindern, dass unbemerkte Schema-Abweichungen in Produktion zu Herausforderungen und Aufwand führen, und die Integrität zwischen Anwendung und Datenbank über alle Deployments hinweg gewährleistet ist oder die Durchsetzung von semantischen Schema-Versionen eine Anforderung ist, kann über die Funktionalität der Eigenentwicklung zur Versionierung eine Schema-Versionskontrolle auf Anwendungsebene eingebunden werden.

2.1.2. Bewährte Praktiken

Die Kombination aus Liquibase Changelogs und Version Enforcement bietet für großskalige Umgebungen Vorteile:

  • Deterministische Kompatibilität: Die Anwendung läuft nur gegen ein getestetes, freigegebenes Schema.

  • Früherkennung: Fehlkonfigurationen oder manuelle Eingriffe werden beim Start erkannt.

3. Release-orientierte Umsetzung von Schemaänderungen

In den folgenden Schritten werden Changelogs in XML als Beispiel verwendet.

Es wird eine Baseline als Ausgangspunkt für die Datenbankstruktur genutzt, auf die alle nachfolgenden Release-Changelogs aufbauen.

Für eine genauere Steuerung, beispielsweise bei Stored Procedures, Triggern oder komplexen Optimierungen, können Changelogs auch als SQL-Dateien integriert werden.

3.1. Verzeichnisstruktur

Die Verzeichnisstruktur ist so aufgebaut, dass die Baseline als erster Schritt dient und alle nachfolgenden Releases über Changelogs (changelog-X.Y.xml) angewendet werden. Sie definiert die grundlegende Datenbankstruktur und bleibt nach der ersten Anwendung unverändert.

/db/changelog/
├── db.changelog-master.xml
├── baseline.xml
├── changelog-1.0.xml
├── changelog-1.1.xml
└── changelog-1.2.xml

Offizielle Dokumentation von Liquibase:

3.2. Root Changelog

Das Root Changelog ist die db.changelog-master.xml und stellt sicher, dass die Baseline zuerst und dann alle weiteren Release-Changelogs in der richtigen Reihenfolge angewendet werden.

Listing 10. Struktur des Root Changelogs
<databaseChangeLog>
    <include file="db/changelog/baseline.xml"/>
    <include file="db/changelog/changelog-1.0.xml"/>
    <include file="db/changelog/changelog-1.1.xml"/>
    <include file="db/changelog/changelog-1.2.xml"/>
</databaseChangeLog>

3.3. Tagging von Baseline und Releases

Vor jedem neuen Release sollte ein Tag gesetzt werden, um ein sauberes Rollback des gesamten Releases zu erleichtern. Der erste Tag definiert die Baseline und markiert diesen Zustand als Referenzpunkt. Dadurch kann bei Bedarf gezielt auf die Baseline zurückgerollt werden. Eine einheitliche sinnvolle Namenskonvention ist empfehlenswert.

Listing 11. Setzen eines Tags
mvn liquibase:tag -Dliquibase.tag=v1.0-baseline
Listing 12. Befehl zum Rollback
mvn liquibase:rollback -Dliquibase.rollbackTag=v1.0-baseline

Offizielle Dokumentation von Liquibase: Maven tag

3.4. Erste Änderungen nach der Baseline

Das erste Update nach der Baseline ist Release-Changelog 1.0. Alle nachfolgenden Release-Changelogs enthalten ausschließlich inkrementelle Änderungen zur Baseline.

Listing 13. Beispiel für inkrementelle Änderungen im Changelog
<databaseChangeLog>
    <changeSet id="1.0-001" author="dev1">
        <addColumn tableName="customers">
            <column name="email" type="varchar(255)"/>
        </addColumn>
    </changeSet>

    <changeSet id="1.0-002" author="dev2">
        <createIndex indexName="idx_orders_date" tableName="orders">
            <column name="order_date"/>
        </createIndex>
    </changeSet>
</databaseChangeLog>

3.5. Preconditions

Preconditions in Liquibase prüfen vor der Ausführung eines Changesets definierte Bedingungen. Wird eine Bedingung nicht erfüllt, kann die Migration abgebrochen, übersprungen oder mit einer Warnung fortgesetzt werden. Dadurch werden Fehler frühzeitig erkannt und nur zulässige Änderungen ausgeführt.

Listing 14. Beispiel für Preconditions
<databaseChangeLog>
    <changeSet id="1.0-001" author="dev1">
        <preConditions onFail="HALT">
            <not>
                <columnExists tableName="customers" columnName="email"/>
            </not>
        </preConditions>
        <addColumn tableName="customers">
            <column name="email" type="varchar(255)"/>
        </addColumn>
    </changeSet>

    <changeSet id="1.0-002" author="dev2">
        <preConditions onFail="HALT">
            <not>
                <indexExists tableName="orders" indexName="idx_orders_date"/>
            </not>
        </preConditions>
        <createIndex indexName="idx_orders_date" tableName="orders">
            <column name="order_date"/>
        </createIndex>
    </changeSet>
</databaseChangeLog>

Offizielle Dokumentation von Liquibase: Preconditions

3.6. Manuelles Rollback im Changelog

Rollback-Anweisungen in der changelog.xml ermöglichen Rollbacks einzelner Changesets für mehr Kontrolle.

Listing 15. Beispiel für ein Rollback im Changelog
<databaseChangeLog>
    <changeSet id="1.0-001" author="dev1">
        <addColumn tableName="customers">
            <column name="email" type="varchar(255)"/>
        </addColumn>
        <rollback>
            <dropColumn tableName="customers" columnName="email"/>
        </rollback>
    </changeSet>

    <changeSet id="1.0-002" author="dev2">
        <createIndex indexName="idx_orders_date" tableName="orders">
            <column name="order_date"/>
        </createIndex>
        <rollback>
            <dropIndex indexName="idx_orders_date" tableName="orders"/>
        </rollback>
    </changeSet>
</databaseChangeLog>

3.7. Befehle zum Rollback

Befehl Bedeutung

mvn liquibase:rollback -Dliquibase.rollbackTag=TAG_NAME

Rollback auf einen zuvor gesetzten Tag (TAG_NAME).

mvn liquibase:rollback -Dliquibase.rollbackCount=1

Rollback um eine bestimmte Anzahl an Changesets (hier: 1 Changeset).

mvn liquibase:rollback -Dliquibase.rollbackDate=YYYY-MM-DDTHH:MM:SS

Rollback auf einen spezifischen Zeitpunkt im Format YYYY-MM-DDTHH:MM:SS.

mvn liquibase:rollbackToDate -Dliquibase.rollbackDate=YYYY-MM-DD

Rollback auf ein bestimmtes Datum (YYYY-MM-DD).

Offizielle Dokumentation von Liquibase: Liquibase Rollback Workflow

4. Einsatz von CI/CD-Pipelines für automatisierte Migrationen

Liquibase kann nahtlos in GitLab CI/CD-Pipelines integriert werden, um Datenbankmigrationen automatisiert, sicher und kontrolliert auszuführen. Die erforderlichen Konfigurationen werden in der Datei .gitlab-ci.yml definiert.

1. Validierung der Changelogs

Vor dem Deployment sollte sichergestellt werden, dass alle Changelogs validiert und fehlerfrei sind. Tritt ein Fehler auf, wird der Prozess abgebrochen, um fehlerhafte Migrationen zu verhindern.

Listing 16. Beispiel für ein Validierungsskript
liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASSWORD validate
2. Deployment auf Staging

Die Datenbankmigration wird durchgeführt und das Release mit einem Tag versehen, um Rollbacks zu ermöglichen. Mit liquibase history werden die durchgeführten Änderungen angezeigt. Falls alle Tests erfolgreich sind, wird das Deployment für die Produktion freigegeben.

Listing 17. Beispiel für ein Deployment-Skript
liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASSWORD update
liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASSWORD tag $RELEASE_VERSION
liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASSWORD history
3. Deployment in Produktion

Die gleichen Deployment-Befehle wie für Staging werden auf der Produktionsdatenbank ausgeführt.

4. Automatisches Rollback bei Fehlern

Falls die Pipeline einen Fehler erkennt, kann ein automatisches Rollback erfolgen:

Listing 18. Beispiel für ein Rollback-Skript
liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASSWORD rollbackToTag $RELEASE_VERSION
liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASSWORD history

Ähnliche Konfigurationen können zum Beispiel für GitHub Actions erstellt werden.

Offizielle Dokumentation von Liquibase: Using Liquibase with GitLab CI/CD

5. DB Update Management: Drei-Phasen-Strategie mit Liquibase

Dieses Verfahren wird angewendet, wenn Daten in einem vorhandenen Datenbankfeld verändert werden müssen. Dabei wird die Anwendung so erweitert, dass sie auf dem neuen Feld arbeitet. Gleichzeitig bleibt die Datenbank in einem Zustand, in dem ältere Anwendungsversionen weiterhin ohne Anpassungen auf der Datenbank arbeiten können. Sobald alle Altversionen abgeschaltet sind, wird dann in einer dritten Phase das alte Datenbankfeld aus der Datenbank entfernt.

1. Phase

Schema- und Migration-Changeset mit Label pre/db-deploy nutzen (z.B. <addColumn> und Trigger oder Batch-SQL nutzen um Bestandsdaten zu kopieren. Hier sollte die Rollback-SQL bereitgehalten werden.

2. Phase

Das Feature-Rollout findet in der Anwendung statt. Hier wird auf neues Feld umgestellt. Falls nötig, ist ein Changeset für zusätzliche Indizes mit dem Label app-deploy zu nutzen.

3. Phase

Laufen alle Instanzen der Anwendung auf der neuen Version der Anwendung ist mit einem Changeset mit dem Label clean-up das Datenbankschema zu bereinigen (z.B. <dropColumn>).

Für CI-Pipelines sind diese Tätigkeiten am besten in einzelnen Jobs auszuführen.

Mindestens jedoch in zwei, Phase 1 und Phase 2 können zusammen ausgeführt werden.

6. Nützliche Liquibase-Befehle zur Schema-Verwaltung

Liquibase bietet verschiedene Befehle zur Überprüfung der Aktualität des Schemas und zur Verwaltung von Änderungen. Hier sind einige der wichtigsten Befehle:

Befehl Bedeutung

liquibase validate

Überprüft, ob alle Changesets korrekt formatiert und konsistent sind. Falls Probleme auftreten, gibt Liquibase eine Fehlermeldung aus, die auf fehlerhafte oder fehlende Changesets hinweist.

liquibase update

Überträgt alle noch nicht angewendeten Änderungen aus den Changelogs auf die Datenbank. Für Datenbanken in der Produktion ist ein Ad-hoc-Snapshot vor dem Liquibase-Update zu erstellen.

liquibase status

Zeigt an, welche Änderungen noch nicht auf die Datenbank angewendet wurden. Liquibase gibt eine Liste der ausstehenden Changesets zurück.

liquibase diff

Vergleicht zwei Datenbanken und zeigt Unterschiede an. Dies ist unter anderem nützlich, um zu überprüfen, ob und wie sich die Datenbanken in verschiedenen Umgebungen unterscheiden. Oder um zu überprüfen, ob alle Objekte in einer Baseline berücksichtigt sind. Hierfür können die Unterschiede zwischen dem IST-Zustand und einer leeren Datenbank erzeugt werden.

liquibase label

Labels können genutzt werden, um Datenbankänderungen, die ein Feature betreffen, zu kennzeichnen und zum gewünschten Zeitpunkt auszuführen. (Vgl. Liquibase-Doku-Labels). Dies kann in Verbindung mit Feature-Toggles genutzt werden.

liquibase snapshot

Erstellt eine Momentaufnahme der aktuellen Datenbankstruktur, die zum Beispiel für Analysen oder den Vergleich mit späteren Versionen verwendet werden kann.

liquibase rollback

Führt ein Rollback aus. Dies stellt den Zustand der Datenbank auf einen definierten Punkt zurück und entfernt alle nachfolgenden Änderungen.

Offizielle Dokumentation von Liquibase: Liquibase-Befehlen.