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.
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.
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.
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.
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:
Diese Dateien werden in folgender Ordnerstruktur verwaltet und im Anschluss genauer beschrieben:
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:
# 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.
<?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.
-- 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:
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.
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
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:
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.
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:
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:
liquibase --url=jdbc:postgresql://192.168.1.20:5432/my_database --username=postgres --password=postgres update
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:
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:
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.
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!