Bild: Lampe

Blog
Einfache Versionskontrolle für Datenbanken mit Liquibase

Darstellung einer Datenbank
Bild: weisser Pfeil

Wer kennt es nicht?

  • Ein kleines Softwareprojekt mit Datenbankanbindung fängt an zu wachsen.
    Datenbank-Aktualisierungen werden über Update-Scripts manuell eingespielt
  • Es wird komplizierter: Was wurde auf der Zielumgebung bereits eingespielt?
    Ein Feld mit Versionsnummer wird in die Datenbank eingeführt
  • Der Build-Server soll Update-Scripts automatisch einspielen.
    Es muss ein Tool geschrieben werden, dass die Überprüfung der Version und das Einspielen der Update-Scripts erledigt

Nun wäre es möglich, ein kleines Tool zu schreiben, welches die Datenbankaktualisierungen einspielt. Jedoch wird hier schnell klar, dass ein solches Tool eine Reihe von Anforderungen erfüllen muss und der Umsetzungsaufwand somit schnell sehr groß werden kann. 

Wie nun das oben beschriebene Problem mit Liquibase aufgelöst bzw. ein Projekt mit Datenbankanbindung bereits zu Beginn in kontrollierte Bahnen gelenkt werden kann, beschreibt dieser Artikel.

Was ist Liquibase?

Liquibase ist eine datenbankunabhängige Open-Source-Bibliothek zum Verfolgen, Verwalten und Anwenden von Änderungen im Datenbankschema und verwaltet eine Art fortlaufend geschriebenen Datenbank-Changelog. Dieser Changelog referenziert UPDATE-Script-Dateien und wird in eigenen Tabellen der zu aktualisierenden Datenbank verwaltet. Der Zugriff auf die jeweilige Datenbank funktioniert über einen JDBC-Treiber mit unterschiedlichsten Datenbanken, wie zum Beispiel PostgreSQL, MySQL, Microsoft SQL-Server, mongoDB, Amazon Redshift und vielen anderen. In unserem Fall verwendeten wir PostgreSQL.

Die Ausgangsbasis der Changelogs kann entweder ein leeres Datenbankschema, also ein CREATE-Script sein, oder irgendein bereits befüllter Datenbestand. Letzteres ist nützlich, wenn eine bereits bestehende Datenbank zukünftig mit Liquibase aktualisiert werden soll und die Erstellung eines CREATE-Scripts (möglicherweise inklusive Initial-Daten) zu aufwändig oder unnötig wäre. Der aktuelle Produktiv-Stand kann in diesem Fall als Startpunkt für den Liquibase Changelog angesehen werden. Die vom Changelog referenzierten UPDATE-Script-Dateien können ebenfalls in unterschiedlichen Formaten, wie SQL, YAML, XML und JSON verarbeitet werden. In unserem Fall entschieden wir uns für das SQL-Format.


Die Vorbereitung

Um Liquibase unter Windows in einem Datenbank-Projekt nutzen zu können, müssen zunächst einige Vorbereitungen getroffen werden – die Installation und die Konfiguration.

Die Installation

Auf allen Computern, von denen aus eine Aktualisierung der Datenbank durchgeführt werden soll, muss Liquibase installiert werden.
Die passende Version findet sich unter: https://www.liquibase.org/download

Wir installierten Liquibase auf den Entwickler-Rechnern und auf dem Build-Server, damit dieser die Updates auf der Zielumgebung automatisch ausführen kann. Darüber hinaus empfiehlt es sich, den Liquibase-Installationspfad (z.B. C:\Program Files\liquibase) in die Windows-PATH-Variable des jeweiligen Computers zu schreiben, damit das Tool komfortabel über die Kommandozeile ansteuerbar ist.

Die Konfiguration

Es gibt, wie oben bereits erwähnt, verschiedene Arten, Liquibase zu nutzen. Wir benötigten es für eine PostgreSQL-Datenbank und wollten SQL-UPDATE-Scripts verwenden.

Für die Verwendung von Liquibase sind folgende Dateien notwendig:

  • Liquibase Konfiguration (liquibase.properties)
  • Changelog Konfiguration (changelog.master.xml)
  • JDBX Treiber (postgresql-42.2.18.jar)
  • Update Scripts (sql-Dateien)

Diese Dateien werden in folgender Ordnerstruktur verwaltet und im Anschluss genauer beschrieben:

ordnerstruktur

 

Liquibase Konfiguration - liquibase.properties

In dieser Datei werden allgemeine Einstellungen wie der Datenbankzugang (Connection String & Anmeldeinformationen) und die zu verwendenden Changelog-Konfigurations- und JDBX-Treiber-Datei festgelegt:

 liquibase.properties
# Enter the path for your changelog file.
changeLogFile=changelog.master.xml

#### Enter the Target database information ####
url=jdbc:postgresql://localhost:5432/my_database
username: postgres
password: postgres

#### JDBC-Driver
classpath: lib/postgresql-42.2.18.jar

#### don't use hub
liquibase.hub.mode=off


Erklärung der Parameter:

Parameter Erklärung Beispiel
 changeLogFile Relativer Pfad zur Changelog-Konfigurationsdatei  changelog.master.xml
 url Datenbank Connection String  jdbc:postgresql://localhost:5432/my_database
 username Datenbank Benutzername  postgres
 password Datenbank Passwort  *****
 classpath Der relative Pfad zur JDBC-Treiber Datei  lib/postgresql-42.2.18.jar
 liquibase.hub.mode

Steuert, ob Liquibase-Hub genutzt werden soll. 
Liquibase-Hub dient zum Monitoring/Reporting von
Datenbank-Release-Automatisierungen
 off



Changelog Konfiguration - changelog.master.xml

Wir haben uns entschieden, unsere SQL-Update-Scripts (bzw. unseren Changelog in SQL ) für Liqubase in mehrere SQL-Dateien zu splitten. Hierbei erstellen wir pro Version eine SQL-Datei, in welcher der für diese Version benötigte Changelog liegt.

Um nun komfortabel alle diese SQL-Dateien in Liquibase zu inkludieren bzw. einen zusammenhängenden Changelog über alle Versionen hinweg zu definieren, wird ein so genannter Master-Changelog  erstellt. Dieser ist in XML geschrieben und inkludiert alle SQL-Dateien, welche im Ordner "scripts" zu finden sind. Für den Master-Changelog wurde XML als klassische Konfigurationssprache gewählt. Dieses Vorgehen folgt einer Empfehlung in der Liquibase-Dokumentation.

 changelog.master.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.8.xsd
   http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-3.8.xsd">

   <includeAll path="scripts/" relativeToChangelogFile="true" />

</databaseChangeLog>


JDBC Treiber

Liquibase arbeitet, wie bereits beschrieben, mit JDBC Treibern. Die zu verwendenden Treiber sollten in einem lib-Verzeichnis neben den Konfigurationsdateien abgelegt werden. Für PostgreSQL kann der passende JDBC-Treiber hier runtergeladen werden.

UPDATE-Scripts

Im Ordner scripts, neben den Konfigurationsdateien, liegen die UPDATE-Scripts, die Liquibase für die Aktualisierung der Datenbank verwenden soll. Eine für Liquibase verwendete SQL-Datei muss immer bestimmte Anweisungszeilen in Form von SQL-Kommentaren beinhalten, die vorgeben, wie Liquibase die Daten verarbeiten soll. In einer solchen SQL-Datei werden mehrere sogenannte "changesets" definiert. Diese beinhalten als Metadaten den Namen des Erstellers, den Zeitstempel der Erstellung sowie einen Kommentar.

Das folgende Beispiel für ein solches SQL-Script, beinhaltet zwei Changesets, die einem CREATE-Script für eine Datenbank entsprechen.

 00000_1.0.0.sql
-- liquibase formatted sql

-- changeset testuser:2021-01-01
CREATE TABLE "public".todo (
   id           integer DEFAULT nextval('configuration_id_seq'::regclass) NOT NULL ,
   description  text ,
   CONSTRAINT pk_configuration_id PRIMARY KEY ( id )
);

-- changeset testuser:2021-01-02
-- comment: insert initial todos
INSERT INTO "public".todo( description ) VALUES ('Entwicklungsumgebung aufsetzen');

Weitere Informationen zum Erstellen solcher Changesets finden sich in der offiziellen Dokumentation:
https://docs.liquibase.com/concepts/basic/sql-format.html

Diese Changesets laufen jeweils innerhalb einer Datenbank-Transaktion ab. Liquibase prüft dann bei Ausführung der Datenbankaktualisierung, ob das Changeset in der jeweiligen Datei bereits ausgeführt wurde oder nicht. In den zwei von Liquibase erstellten Tabellen databasechangelog  und databasechangeloglock  führt Liquibase Buch darüber, welche Updates bereits auf der jeweiligen Datenbank ausgeführt wurden.

Auf Basis des oben dargestellten SQL-Scripts ergeben sich somit folgende Einträge in der Liquibase-Tabelle databasechangelog:

Ansichtr: Liquibase-Tabelle


Die erste Spalte id  eines solchen Changelog-Eintrags ist freibleibend und kann nach Belieben definiert werden, sie sollte jedoch innerhalb einer Datei einzigartig sein. Wir verwendeten hierfür das jeweils aktuelle Datum. Es könnten jedoch auch Ticket-Nummern oder sonstige Informationen genutzt werden. Wurde ein Changelog bereits in eine Datenbank eingespielt, kann diese nicht mehr verändert werden, da Liquibase einen MD5-Hash über den Inhalt des Changelogs macht und diesen bei Ausführung prüft.

Changelogs können durch Contexts  und Tags  angereichert werden. Auf dieses Weise können unter anderem umgebungsspezifische Aktualisierungen erstellt werden. Nutzt man beispielsweise einen Kontext TEST  dann werden die so gekennzeichneten SQL-Anweisungen nur auf einem System mit der Umgebung TEST  ausgeführt .

Weiterhin sei erwähnt, dass zusätzlich zu den Changelogs auch Rollbacks definiert werden können, da Liquibase in der Lage ist, Datenbanken auf bestimmte Vorgängerversionen "downzugraden". Leider funktionierte das jedoch in unserem Projekt nicht zufriedenstellend, so dass immer ergänzend weitere Anpassungen durch manuell definierte Rollbacks vorgenommen werden mussten.

Weitere Informationen zum Umgang mit den Update-Scripts bzw. insgesamt zur Funktionalität von Liquibase finden sich in der Liquibase-Dokumentation.

 

Die Initialisierung

Möchte man bei einem bestehenden Projekt, welches anfangs noch klein war und dann stetig gewachsen ist, eine Datenbankversionierung einführen, bietet Liquibase hierfür zwei Möglichkeiten:

  • Komplette Datenbank versionieren
    • Datenbank-Changelog für die komplette Datenbank generieren lassen (also ein CREATE-Script) und dann jede Änderung im Changelog erfassen
    • Vorteil: Liquibase kann auch leere Datenbanken automatisch bespielen.
    • Nachteil: Generierter Initial-Changelog muss kontrolliert und ggf. berichtigt werden; Möglicherweise viel Aufwand für die Befüllung der Datenbank mit Standard-Daten.
  • Datenbank ab einem definierten Stand versionieren
    • Vorteil: Datenbankversionierung ab sofort.
    • Nachteil: Leere Datenbanken müssen manuell bespielt werden.

 

Komplette Datenbank versionieren

Bei bestehenden Datenbanken kann ein Initial-Changelog (CREATE-Script) generiert werden. 

Im Folgenden wird ein Befehl gezeigt, mit dessen Hilfe ein Changelog im SQL-Format erstellt wird. Es ist ebenfalls möglich, den Changelog im XML-Format generieren zu lassen. Hierbei muss einfach die Dateieendung im folgenden Befehl auf .XML geändert werden. (weitere Informationen: https://docs.liquibase.com/commands/community/generatechangelog.html)

liquibase --changeLogFile=scripts/initial.changelog.generated.sql generateChangeLog



Datenbank ab einem definierten Stand versionieren

Ist das Ziel eine Datenbank ab einem bestimmten Stand zu versionieren, so muss zunächst der Startzeitpunkt definiert werden. 

Wird hier beispielsweise die Produktiv-Datenbank als Start gewählt, so müssen alle Update-Scripts ab der Produktivversion in das gewünschte Liquibase-Format gebracht werden. Liegen bereits Update-Scripts im SQL Format vor, die bisher händisch ausgeführt wurden, empfiehlt es sich, wie in unserem Fall, das SQL-Format auch für die Liquibase Scripts zu verwenden, da hierbei nur ein paar Liquibase-Keywords in die Update-Scripts eingefügt werden müssen (wie zB "-- liquibase formatted sql", siehe den Punkt "Update-Scripts" unter "Die Konfiguration"). Weiterhin müssen die alten Update-Scripts dann an einen für Liquibase erreichbaren Ort kopiert werden. Bei uns wurde, wie oben erwähnt, hierfür der Ordner /scripts  verwendet (siehe "Die Konfiguration").

Konkret heißt das:

  • Kopiere alle *.SQL-Update-Scripts von der Produktivversion bis zur Testversion in den Ordner /scripts
  • Füge die Liquibase-Keywords in jedes Update-Scripts ein (auch die "changeset"-Anweisungen, zumindest eine pro Datei)
  • Benenne die Dateien, falls noch nicht geschehen, in einer logisch aufsteigenden Reihenfolge 

Angenommen unsere Entwickler-Datenbank-Version entspricht der Test-Version, so müssen wir Liquibase mitteilen, dass wir in der Vergangenheit bereits alle diese Update-Scripts ausgeführt hatten.

Dies passiert mit folgendem Kommandozeilenaufruf:

liquibase changeLogSync

Dies führt nun dazu, dass alle Liquibase-Verwaltungstabellen in unserer Datenbank angelegt werden und alle vorhandenen, alten Update-Scripts (neu: Changelogs mit Changesets) in diese Verwaltungstabellen als "bereits ausgeführt" eingetragen werden.

 

Die Ausführung

Einfacher Aufruf

Wird nun eine Konsolenfenster im Ordner, in dem unsere Datei liquibase.properties  (Standardkonfiguration) liegt, geöffnet, so kann mit folgendem Befehl ein einfaches Update auf die konfigurierte Datenbank erfolgen:

liquibase update

Dieser Befehl führt dazu, dass Liquibase mit den Standardparametern aus liquibase.properties  ausgeführt wird. Jeder dieser Parameter kann jedoch auch explizit angegeben, der dazugehörige Standardparameter also für diesen Aufruf überschrieben werden. In unserem Fall entspräche der obige Befehl dem Aufruf:

 update local db
liquibase --url=jdbc:postgresql://localhost:5432/my_database --username=postgres --password=postgres --classpath=lib/postgresql-42.2.18.jar update

Hierbei können auch einzelne Parameter weggelassen werden, sofern sie in der Standardkonfiguration enthalten sind. Bei den nun folgenden Beispielen lassen wir darum den Parameter classpath  weg, da der JDBC-Driver in all diesen Aufrufen gleich bleibt und somit der Eintrag aus der Standardkonfiguration passt.

Ein Update auf eine Datenbank, die nicht auf dem lokalen PC, sondern zum Beispiel auf dem Rechner mit der IP 192.168.1.20 läuft, könnte beispielsweise wie folgt aussehen:

 update test db
liquibase --url=jdbc:postgresql://192.168.1.20:5432/my_database --username=postgres --password=postgres update
Auf diese Weise können zum Beispiel die Aufrufe vom Build-Server entsprechend der jeweiligen Zieldatenbank unterschiedlich zu denen für die Entwicklerrechner angepasst werden. 


Konfiguration unterschiedlicher Zielumgebungen

In der Regel sind, vor allem bei Web-Projekten, mehrere Zielumgebungen mit unterschiedlichen Versionen vorhanden. In unserem Fall haben wir zum Beispiel eine lokale Entwicklungsdatenbank und eine Testdatenbank auf einem TEST-System.

Möchte man die Konfiguration mehrerer Zielumgebungen per Liquibase realisieren, so gibt es hier hierfür 2 Möglichkeiten:

Aufruf von Liquibase mit den entsprechenden Parametern
Wie oben beschrieben, kann man durch die explizite Angabe der Parameter die Standardparameter für einen Aufruf von Liquibase überschreiben und somit zum Beispiel auf dem Build-Server eine andere Zieldatenbank verwenden:

 update test db
liquibase --url=jdbc:postgresql://192.168.1.20:5432/my_database_TEST --username=theSecretUsername --password=theSecurePassword update

Nutzen mehrerer Konfigurationsdateien
Möchte man die Zielumgebungskonfigurationen gerne in separate Dateien ablegen, so bietet es sich an, hier pro Zieldatenbank eine eigene Datei analog der Datei mit den Standardparametern liquibase.properties  anzulegen (siehe Kapitel: Die Konfiguration).
Für eine Test-Instanz könnte die Datei beispielsweise liquibase.test.properties  heißen.
Durch die explizite Angabe der zu verwendenden Konfigurationsdatei kann Liquibase auf diese Weise mit dem passenden Parametersatz aufgerufen werden:

 update test db
liquibase --defaultsFile=liquibase.test.properties

Natürlich können solche Aufrufe ohne Weiteres auch im Build-Server integriert werden, so dass dieser für jede Umgebung immer den passenden Parametersatz verwendet.


Wie man sieht, ist es mit recht wenig Aufwand möglich, eine saubere und skalierbare Datenbankversionierung bzw. Datenbank-Aktualisierungsroutine zu schaffen, welche einfach in den Build-Server eingehängt werden kann um automatische Datenbank-Updates bei jedem Deployment zu ermöglichen. Hat man diesen Prozess einmal eingerichtet, ist es ein Leichtes, ihn auch in anderen Projekten zu implementieren und es sind nie wieder manuelle Sonderlösungen oder speziell geschriebene Tools nötig.

Wollt ihr mehr über Datenbankversionierung für Liquibase erfahren?
Hier findet ihr weitere Informationen: https://www.liquibase.org/

Bei Anmerkungen und Fragen zögert nicht, einen Kommentar zu hinterlassen.

    Autor des Artikels: Andreas Willert
    Autor des Artikels:

    Andreas Willert, B.Sc. Wirtschaftsinformatik

    Seit über 5 Jahren erarbeite ich Lösungen zu Problemen im Bereich der Softwareentwicklung und bin derzeit primär in der Webentwicklung tätig. Frei nach dem Motto: Tschakka, wir schaffen das. Es gibt keine Probleme, sondern Lösungen!

     

TOP
Wir benutzen Cookies

Wir nutzen Cookies auf unserer Website. Einige von ihnen sind essenziell für den Betrieb der Seite, während andere uns helfen, diese Website und die Nutzererfahrung zu verbessern (Tracking Cookies). Sie können selbst entscheiden, ob Sie die Cookies zulassen möchten. Bitte beachten Sie, dass bei einer Ablehnung womöglich nicht mehr alle Funktionalitäten der Seite zur Verfügung stehen.