Über unsMediaKontaktImpressum
Marcus Nörder-Tuitje 11. März 2025

Liquibase, eine detaillierte Betrachtung: Evolution in der Datenbankverwaltung

Datenbanken sind das Herzstück moderner Software-Projekte – und genau hier entstehen oft die größten Herausforderungen. Versionierung, Migration und Konsistenz von Datenbankänderungen können schnell zu einem Minenfeld werden. Ein falscher Schritt und plötzlich laufen Migrationen oder Prozesse ins Leere. Doch es gibt Hoffnung: Liquibase, ein Tool, das den Umgang mit Datenbankänderungen nicht nur vereinfacht, sondern regelrecht revolutioniert hat.

Dieser Artikel beleuchtet, wie Liquibase Teams dabei unterstützt, komplexe Datenbankmigrationen sicher, automatisiert und transparent zu gestalten. Vom Konzept der ChangeSets bis hin zu Rollbacks und Umgebungsabhängigkeiten: Wir zeigen Ihnen, wie Sie Liquibase effektiv einsetzen können, um Chaos in geordnete Bahnen zu lenken – und Ihre Software-Projekte auf die nächste Stufe zu heben. Inklusive Praxisbeispiele mit Spring Boot!

Liquibase: Was macht dieses Tool aus?

Die Verwaltung von Datenbanken stellt in der Software-Entwicklung oft eine Herausforderung dar. In unseren Kundenprojekten arbeiten wir in der Regel mit mindestens zwei Umgebungen: Akzeptanz und Produktion. Zusätzlich gibt es mindestens eine Entwicklungsumgebung und die Entwickler selbst nutzen oft noch einen separaten DB-Container. So kommt eine Menge von koexistierenden Umgebungen zusammen. Die manuelle Durchführung von Änderungen ist fehleranfällig, zeitaufwändig und schwer nachvollziehbar. Genau hier entfaltet Liquibase seine Stärken und spielt seinen entscheidenden Vorteil aus. Liquibase adressiert diese Probleme durch:

  • Automatisierung von Datenbankänderungen
  • Integration in CI/CD-Pipelines
  • Propagation von Änderungen an die Entwickler durch simples git update/git merge
  • Versionskontrolle von Änderungen
  • Historisierung der durchgeführten Migrationen
  • Rückgängigmachen von fehlerhaften Änderungen.

Seine datenbankunabhängige Struktur und Erweiterbarkeit machen es zu einem idealen Werkzeug für moderne Software-Projekte. Liquibase hält die ChangeSets nach und speichert die Prüfsummen, dies stellt die Integrität eines Modells sicher. Beim Starten der Anwendung vergleicht Liquibase die Versionen, die in der Datenbank hinterlegt sind, mit der Version, die mit der Anwendung kommt. Sind nicht alle Änderungen konsistent, dann startet die Anwendung nicht und Liquibase gibt eine Fehlermeldung aus. D. h. die Datenbank mit jeglicher vorheriger – mit Liquibase verwalteter – Version kann auf die letzte Version migriert werden; Abwärtsmigration indes ist nicht möglich.

Konzepte von Liquibase

Liquibase operiert mit den folgenden zentralen Konzepten:

  1. ChangeSets: sind grundlegende Einheiten, die spezifische Änderungen beschreiben. Ein ChangeSet umfasst mehrere Changes, die Änderungen durchführen und können verschiedene Vorbedingungen wie z. B. Datenbanken, Profile, jedoch auch komplexe Vorbedingungen beinhalten.
  2. ChangeLogs: Sammlungen von ChangeSets, die Änderungen an der Datenbank definieren. Ein ChangeLog entspricht einer Datei, in der verschiedene ChangeSets aneinandergereiht werden.
  3. Rollbacks: Mechanismen, um Änderungen bei Bedarf rückgängig zu machen. Dies sind die Kompensationsmaßnahmen, die durchgeführt werden, wenn ein ChangeSet fehlschlägt.
  4. Kontextabhängige Ausführung: ermöglicht, Änderungen nur in bestimmten Umgebungen anzuwenden.
  5. Idempotenz: Sicherstellung, dass jede Änderung nur einmal ausgeführt wird.

Die nachfolgend gezeigten Beispiele sind in XML. Ein schöner Nebeneffekt an der XML-Notation ist, dass man eine volle Tooling-Unterstützung bekommt. Das macht die sperrige Syntax erträglicher und ein intuitives Arbeiten ist ohne Probleme möglich.

Liquibase: ChangeLog Master

Der ChangeLog Master führt alle ChangeLogs zusammen und definiert die Reihenfolge, in der ChangeLogs abgespult werden. Ein ChangeLog in einem Spring-Boot-Container kann nicht nur ChangeLogs aus dem Projekt referenzieren, sondern auch aus dem Classpath oder ganze Verzeichnisse:

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
         www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"&gt;
    <include file="classpath:/db/changelog/ChangeSets/myframework-1.0.0.xml"/>
    <include file="db/changelog/extra/12-01-005-database-specific.xml"/>
    <includeAll path="db/changelog/2024"/>
</databaseChangeLog>

Dabei wird die Reihenfolge entlang der Einträge des ChangeLog Masters definiert; ganze Verzeichnisse werden durch eine Nomenklatur sortiert und abgearbeitet.

Liquibase: ChangeLog

Ein ChangeLog ist eine Zusammensetzung von verschiedenen ChangeSets. Jedes ChangeSet wird nach Abarbeitung in der Tabelle DB_CHANGELOG nachgehalten. Damit wird sichergestellt, dass kein ChangeLog doppelt abgespielt wird. Aber Vorsicht: Die ChangeLogs werden mit einer Prüfsumme gespeichert, ein nachträgliches Ändern ist nicht ohne manuelle Eingriffe möglich – und sollte bei einem Produktivsystem ohnehin nicht gemacht werden. Hierbei gilt: Die Lösung für ein fehlerhaftes oder unvollständiges ChangeLog ist ein weiteres ChangeLog.

Liquibase: ChangeSets und die detaillierte Betrachtung der Einstellungen

Grundlagen

ChangeSets sind der Kern von Liquibase. Sie definieren, welche Änderungen an einer Datenbank vorgenommen werden sollen. Im Folgenden werden die wichtigsten Einstellungen von ChangeSets und ihre Anwendungsfälle beleuchtet. Alle Entwickler haben stets die neuesten Datenbankänderungen zur Verfügung. Wenn jedoch zwei unterschiedliche Feature Branches gleichzeitig Datenbankänderungen enthalten, wird zunächst die erste Änderung übernommen, da das Liquibase-Log noch konsistent ist. Beim zweiten Merge kommt es beim Deployment zu einem Fehler, da beide Änderungen dieselbe ID verwenden. In diesem Fall muss die nächste freie ID gewählt werden und der Entwickler muss seine Datenbank bereinigen. Jedes ChangeSet wird durch eine Kombination aus ID, Autor und Datei eindeutig identifiziert. Dies ermöglicht es Liquibase nachzuverfolgen, welche Änderungen bereits angewendet wurden.

Hierbei noch einmal der Hinweis, dass die ChangeSets, welche verarbeitet wurden, in der Datenbank mit Prüfsumme vorgehalten werden, um sicherzustellen, dass sie nur einmal abgearbeitet werden und nur nachträgliche – also konsistente Änderungen – möglich sind.

Das Attribut "author" dient dazu, den Urheber der Änderung anzugeben. Dies erleichtert die Nachvollziehbarkeit und das Management von ChangeSets. Die Dimensionierung, was alles ein ChangeSet ist, kann man an der Schneidung des Datenmodells festmachen. Bei einem klassischen 3NF-Modell mit mehreren Root Entities lohnt sich die Klammerung auf Änderungen, die eine Tabelle betreffen, zu bündeln; bei Modellen, die nur eine Root Entity enthalten, bündelt man am besten nach Fachlichkeit. Wenn die Strategie einem späteren Leser bekannt ist, lassen sich so Rückschlüsse auf die Intention der durchgeführten Anpassungen schließen. Selbstverständlich sind das nur Best Practices; das Tool selbst macht keine Vorgaben.

Liquibase: Naming

Grundsätzlich gilt bei allen Liquibase Changes, dass man zwar datenbankagnostisch ist, aber nicht die Grenzen des Zielsystems sprengen darf. Bestes Beispiel hierfür ist die seit Ewigkeiten auf 31 Zeichen begrenzte Vergabe von Namen in der Oracle-Datenbank (Tabellen, Indizes, Fremdschlüssel etc.). Natürlich kann Liquibase auch nicht wissen, ob ein DBA die MSSQL-Server-Datenbank auf case-sensitives Verhalten eingestellt hat. Dasselbe gilt für das Standardverhalten von MySQL-Datenbanken unter Windows (case-insensitive Tabellennamen) und Linux (case-sensitive Tabellennamen). Hierbei empfiehlt es sich, auf ein einheitliches Naming zu verständigen, also alle Namen lower case oder upper case – aber nicht durcheinander.

Tabellen anlegen

Legen wir los und erstellen unsere erste Tabelle! Sie ist zwar nichts Außergewöhnliches, aber ein solider Start – eine einfache Tabelle mit einer UUID als Primärschlüssel und ein paar Spalten. Das erreichen wir mit dem createTable-Tag. Der Primärschlüssel wird dabei ganz einfach als entsprechender Constraint direkt in der gewünschten Spalte verschachtelt. Da wir das Ganze nur anlegen und nicht an irgendwelche Bedingungen koppeln wollen, ist das hier gezeigte Beispiel sehr einfach:

<changeSet id="initial-tables-000" author="marcusnoerder-tuitje (generated)">
    <createTable tableName="customer">

        <column name="uuid" type="UUID" >
            <constraints nullable="false" primaryKey="true" primaryKeyName="pk_customer"/>
        </column>

        <column name="last_name" type="VARCHAR(200)" />
        <column name="first_name" type="VARCHAR(200)"/>
        <column name="date_of_birth" type="DATE"/>

    </createTable>
</changeSet>

Tabellen erweitern

Mit addColumn kann man ganz unkompliziert neue Spalten hinzufügen. Dabei werden Name und Typ unabhängig von der konkreten Datenbanktechnologie definiert. Es ist auch möglich, mehrere Spalten gleichzeitig anzulegen. Wichtig ist zu beachten: Bei bestehenden Datensätzen darf ein NOT NULL Constraint nur dann gesetzt werden, wenn ein Standardwert definiert ist.

<changeSet id="initial-tables-001" author="marcusnoerder-tuitje (generated)">
    <addColumn tableName="customer">
        <column name="height" type="double"/>
    </addColumn>
</changeSet>

Indizes

Indizes können einfach per ChangeSet nachgeliefert werden:

<changeSet id="initial-tables-002" author="marcusnoerder-tuitje (generated)">
    <createIndex tableName="customer" indexName="idx_customer_lastname">
        <column name="last_name"/>
    </createIndex>
</changeSet>

Fremdschlüssel

Fremdschlüssel lassen sich ähnlich intuitiv anlegen, indem man die Referenzen im ChangeSet beschreibt:

<changeSet id="initial-tables-003" author="marcusnoerder-tuitje (generated)">
    <createTable tableName="customer_note">
        <column name="uuid" type="UUID" >
            <constraints nullable="false" primaryKey="true" primaryKeyName="pk_customer_note"/>
        </column>
        <column name="customer_uuid" type="UUID"/>
        <column name="note" type="varchar(500)"/>
    </createTable>
    <addForeignKeyConstraint baseTableName="customer_note"
                             baseColumnNames="customer_uuid"
                             constraintName="fk_cust_note2customer"
                             referencedTableName="customer"
                             referencedColumnNames="uuid"/>
</changeSet>

PreConditions (Vorbedingungen)

PreConditions definieren, unter welchen Bedingungen ein ChangeSet ausgeführt wird. Sie sind besonders hilfreich, um sicherzustellen, dass Änderungen nur vorgenommen werden, wenn bestimmte Voraussetzungen erfüllt sind. MARK_RAN sagt in unserem Beispiel, dass Liquibase dieses ChangeSet als "erfolgreich durchlaufen" markieren soll, wenn die PreCondition verletzt wurde. Dies ist insbesondere im Kontext bereits bestehender, noch nicht durch Liquibase verwalteter Tabellen sehr nützlich.

Beispiel:

<changeSet id="initial-tables-006" author="marcusnoerder-tuitje (generated)" >
    <preConditions onFail="MARK_RAN" >
        <not>
            <tableExists tableName="old_customer"/>
        </not>
    </preConditions>
    <createTable tableName="old_cusatomer">
        <column name="customername" type="varchar(100)"/>
    </createTable>
</changeSet>

Liquibase: Rollbacks

Liquibase bietet die Möglichkeit, für jedes ChangeSet Rollback-Mechanismen zu definieren, um Änderungen rückgängig zu machen. Dies ist entscheidend, um Fehler schnell beheben zu können. Durch Rollbacks lassen sich vernünftige Kompensationsmaßnahmen definieren, wenn ein ChangeSet scheitert. Da wir in der Individualsoftware-Entwicklung in der Regel exakt wissen, wie die Produktion beim Kunden aussieht und meist sogar beim Kunden die Migration testen können, nutzen wir diese Eigenschaften in unseren Produkten eher nicht. Allerdings gibt es auch Produkthersteller, die mehrere hundert bis tausende Installation "draußen" haben. Dort ist diese Funktion auf jeden Fall sinnvoll; eine Migration lässt sich so um Schutzmaßnahmen und ein entsprechendes Rollback-Verhalten erweitern.

Beispiel:

<changeSet id="initial-tables-008" author="marcusnoerder-tuitje (generated)" >
    <createTable tableName="example_table">
        <column name="id" type="int">
            <constraints primaryKey="true" nullable="false"/>
        </column>
        <column name="name" type="varchar(255)"/>
        <column name="created_at" type="datetime" defaultValueComputed="CURRENT_TIMESTAMP"/>
    </createTable>

    <rollback>
        <dropTable tableName="example_table"/>
    </rollback>
</changeSet>

Kontexte (Umgebungsabhängigkeit)

Mit Kontexteinstellungen können ChangeSets gezielt in bestimmten Umgebungen wie Entwicklung, Test oder Produktion angewendet werden.

Beispiel:

<changeSet id="initial-tables-010" author="marcusnoerder-tuitje (generated)" context="prd">
    <createIndex tableName="CUSTOMER" indexName="IDX_CUST_DOB">
        <column name="DATE_OF_BIRTH"/>
    </createIndex>
</changeSet> 

dbms (Datenbankabhängigkeit)

Mit dem dbms-Attribut lassen sich ChangeSets auf spezifische Datenbanken beschränken. Dies ist insbesondere dann nützlich, wenn man Produktspezifikationen, die außerhalb des Standards liegen, nutzen möchte. Ein Szenario ist beispielsweise, die “kleine” H2 normal aufzubauen, während man die PostgreSQL-Datenbank mit Partitionen ausstattet, weil dort ein ganz anderes Mengengerüst vorliegt.

Beispiel:

<changeSet id="initial-tables-005" author="marcusnoerder-tuitje (generated)" dbms="h2">
    <insert tableName="customer">
        <column name="uuid" value="d709f645-86b1-460c-820b-70ba6f0fc324"/>
        <column name="first_name" value="Hein"/>
        <column name="last_name" value="Hein"/>
    </insert>
</changeSet>

Custom Changes (Eigene Erweiterungen)

Liquibase erlaubt die Implementierung benutzerdefinierter Änderungen durch Java-Klassen. Dies bietet maximale Flexibilität für spezifische Anforderungen.

Beispiel:

<changeSet id="initial-tables-007" author="marcusnoerder-tuitje (generated)" >
    <customChange class="io.crowdcode.liquibase.plugins.demoplugin.GeneratePropertyFileChange">
        <param name="tableName" value="CUSTOMER"/>
        <param name="columnName" value="FIRST_NAME"/>
        <param name="propertyFilePath" value="src/main/resources/liquibase.properties"/>
    </customChange>
</changeSet>

Onboarding einer bestehenden Datenbank

Das Onboarding einer bestehenden Datenbank lässt sich sehr schön mittels PreConditions lösen. Wenn eine Tabelle existiert, nutzt man onFail=”MARK_RUN”, um Liquibase mitzuteilen, dass eine Tabelle bereits existiert. Leider deckt dies nicht automatisch Fremdschlüssel und Indizes ab. Jedes RDBMS hat eigene Strategien, wie diese benannt werden. Hier muss man sich eine dynamische Lösung selbst definieren; z. B. über Liquibase-Plugins, die eventuell Fremdschlüssel und Indizes lokalisieren und abräumen, damit sie später per ChangeLog harmonisiert und versioniert angelegt werden können.

Grenzen von Liquibase

Wie bereits oben angedeutet, kann Liquibase die Arbeit an der Datenbank vereinfachen, ist aber darauf angewiesen, dass der Entwickler sich mit den zu adressierenden Umgebungen befasst. Dazu gehören Datenbankspezifika, versionsbedingte Unterschiede und Konfigurationseinstellungen. Im Idealfall kann eine Anwendung vor dem Rollout einmal mit allen Datenbanktypen getestet werden (z. B. durch CI Build), um sicherzustellen, dass man mit einem Namen, einem Typen oder auf anderem Wege an ein Limit einer Datenbank kommt. Werkzeuge wie Liquibase Linter können hierbei helfen, ggf. muss man allerdings manuell eingreifen.

Datenbanken auf den diesjährigen IT-Tagen

Spannende Vorträge und Workshops zum Thema Datenbanken erwarten Euch auch auf den IT-Tagen, der Jahreskonferenz von Informatik Aktuell. Die IT-Konferenz findet jedes Jahr im Dezember in Frankfurt statt – dieses Jahr vom 08.-11.12.

Liquibase: Fazit

Liquibase bietet eine strukturierte und effiziente Lösung für das Management von Datenbankänderungen. Liquibase ist sowas wie das Git für Datenbanken. Durch die verschiedenen Einstellungen der ChangeSets können Entwickler Änderungen sicher und flexibel umsetzen. Mit klaren PreConditions, Rollbacks und Kontexten wird sichergestellt, dass Änderungen nachvollziehbar und kontrolliert bleiben. Für Teams, die auf Konsistenz und Automatisierung setzen, ist Liquibase ein unverzichtbares Werkzeug – im Kontext datenbankgetriebener Entwicklung nimmt es bei uns einen ähnlich hohen Stellenwert wie Git ein.

Richtig eingesetzt bietet Liquibase die Möglichkeit, das Datenmodell mit den Änderungen auszurollen und stets einen konsistenten Stand zu haben. Dadurch wird eine Datenbank automatisch immer aufwärtskompatibel und auch Migrationen werden stark vereinfacht.

Autor
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben