Spring Boot und Liquibase

„Die Geschichte ist eine ewige Wiederholung.“

Thukydides

Es existiert kaum ein Software Projekt in dem nicht irgendwelche Daten in einer Datenbank gespeichert werden. Solange sich das Projekt im Entwicklungsstatus befindet, sind vielfache Änderungen am Datenbankschema kein Problem. Gerade bei der Entwicklung mit Datenbankabstraktionen wie Hibernate oder JPA machen sich Entwickler wenig Gedanken, weil das Framework sich um die Aktualisierung des Schemas kümmert.

Spätestens wenn die Software in Produktion verwendet wird, ändert sich dieser gedankenlose Umgang mit den Datenbankstrukturen. Bei Millionen von Tabelleneinträgen möchte kein Entwickler, dass ein geistloses Programm die Datenstruktur selbständig verändert und dabei vielleicht beschädigt. Häufig sind dann Skripte im Einsatz um die aktuellen Datenbankänderungen vorzunehmen. Diese müssen dann zumeist in SQL geschrieben und manuell ausgeführt werden. Das Prozedere ist aufwendig und fehleranfällig. Kommt dann der Auftraggeber auf die Idee, eine Datenbank mit einem anderen SQL Dialekt verwenden zu wollen, dann müssen alle Skripte angepasst werden.

Mit Liquibase gibt es aber seit mittlerweile 15 Jahren eine elegante Lösung für dieses Problem. Liquibase ist eine Bibliothek um Änderungen an einem Datenbankschema verfolgen, verwalten und anwenden zu können. Dazu definiert der Entwickler ein ChangeLog für seine Datenbank in XML, YAML oder JSON Format.

# db.changelog-master.xml
databaseChangeLog:
  - include:
      comments: initial dabase setup with legacy data
      file: db/changelog/0000-init.yaml
  - include:
      comments: add ancestor image tables
      file: db/changelog/0001-ancestor-images.yaml

Die hier dargestellte YAML Datei db.changelog-master.xml verweist mit den beiden include Einträgen auf zwei weitere ChangeLog Dateien, die ihrerseits ChangeSets enthalten. Ein ChangeSet kann wiederum mehrere Changes enthalten, jedoch gehört es zu den Best Practices, nur eine Änderung pro ChangeSet zu definieren.

# 0000-init.yaml
databaseChangeLog:
  - changeSet:
      id: ancestor-member
      author: Jens Kaiser
      comments: Ein Mitglied in einem Ahnenforschungsverein
      changes:
        - createTable:
            columns:
              - column:
                  constraints:
                    nullable: false
                    primaryKey: true
                    primaryKeyName: CONSTRAINT_A
                  name: ID
                  type: BIGINT
              - column:
                  constraints:
                    nullable: false
                  name: NICKNAME
                  type: VARCHAR(6)
              - column:
                  name: GIVEN_NAME
	          constraints:
                    nullable: false
                  type: VARCHAR(255)
              - column:
                  name: SURNAME
                  constraints:
                    nullable: false
                  type: VARCHAR(255)
            tableName: MEMBER

In diesem ChangeLog ist ein einzelnes ChangeSet enthalten, mit dem eine Datenbank Tabelle MEMBER erstellt wird. Obwohl noch nicht viel gezeigt wurde, lassen sich schon interessante Eigenschaften der Liquibase Lösung erkennen. Ganz offensichtlich ist diese Konfiguration nicht datenbankspezifisch. Ob nun H2, MySQL, MariaDB, PostgreSQL, Oracle oder Informix, die YAML Konfiguration erzeugt auf allen Datenbanken eine entsprechende Tabelle. Auch kann der ChangeLog verwendet werden um in verschiedenen Umgebungen identische Datenbanken zu erstellen. Eine zur Produktion identische Testdatenbank kann also jederzeit erzeugt werden. Nicht ganz so offensichtlich ist es, dass diese Änderung von Liquibase automatisch zurückgenommen werden kann. In diesem Fall weiß Liquibase, dass ein createTable durch ein dropTable rückgängig gemacht werden kann.

Bereits existierende Projekte um Liquibase zu ergänzen klingt nach einer ganzen Menge Arbeit, da es nun heißt, ChangeSets für alle Tabellen, Sequenzen und bestehende Daten zu erzeugen. Erfreulicherweise ist dies nicht der Fall, denn Liquibase erlaubt es über die Kommandozeile oder ein Maven Plugin, ein bestehendes Datenbankschema auszulesen und als Changelog zu speichern.

liquibase --changeLogFile=changelog.yml generateChangeLog

Hier wird auf der Kommandozeile das aktuelle Datenbankschema in die ChangeLog Datei changelog.yml geschrieben. Die Informationen zur Datenbank erhält das Tool aus einer speziellen Property-Datei.

Damit sich Liquibase um die Änderungen des Datenbankschemas in einem Spring Boot Projekt kümmert, muss die Bibliothek in den Classpath aufgenommen werden.

<dependency>
  <groupId>org.liquibase</groupId>
  <artifactId>liquibase-core</artifactId>
  <version>4.3.5</version>
</dependency>

Zusätzlich benötigt Liquibase einen Verweis auf das ChangeLog in der application.properties.

spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.yaml
spring.liquibase.enabled=true
spring.jpa.hibernate.ddl-auto=validate

Hier sind zusätzlich die Properties spring.liquibase.enabled und spring.jpa.hibernate.ddl-auto angegeben. Die Property spring.jpa.hibernate.ddl-auto steht auf validate, da sich nun Liquibase um die Änderungen der Datenbank kümmert und zusätzlich ist hier die Property spring.liquibase.enabled angegeben um Liquibase bei der Entwicklung einfach deaktivieren zu können.

Wird die Spring Boot Anwendung nun gestartet, dann prüft Liquibase ob es Änderungen an dem ChangeLog gab. Gibt es noch nicht durchgeführte ChangeSets, dann werden diese ausgeführt. Damit es keine nachträglichen Änderungen an schon ausgeführten ChangeSets gibt, prüft Liquibase diese mit einem MD5 Hash auf eventuelle Änderungen. Nachträgliche Änderungen an einem ChangeSet sind in den meisten Fällen verboten, weil sich sonst das aktuelle Datenbankschema nicht mehr aus dem ChangeLog reproduzieren lässt.

Damit ist die Umstellung eines Spring Boot Projektes auf Liquibase auch schon beendet und zukünftige Änderungen am Datenbankschema können nun automatisiert auf den Zielsystemen installiert werden.