Datenbankversionierung in Spring Boot mit Liquibase

Von |2018-05-19T08:20:39+00:0019. Februar 2018|Software-Entwicklung|0 Kommentare
12 Minuten Lesedauer

TL;DR

Schon einmal vor dem Problem gestanden, dass Ihr zwar eueren Source Code unter voller Versionskontrolle z.B. über GIT habt, aber euer Datenbankschema nach einem überstürzten Rollback nicht mehr dazu gepasst hat? Wenn ja, lohnt sich für euch wahrscheinlich das Weiterlesen, wenn nein… ganz sicher auch 😉

Wer keine Lust hat zu lesen, kann sich auch direkt unser Beispielprojekt anschauen.

Übersicht

Dieses kurze Tutorial zeigt Euch, wie Ihr schnell und unkompliziert einen Workflow zur Datenbankversionierung in Spring Boot einrichteten könnt. Dafür wird Liquibase als Tool verwendet. Spring Boot unterstützt von Haus aus drei verschiedene Möglichkeiten für das Handling von Schemaänderungen in einer Datenbank.

Hibernate, Flyway oder Liquibase

Hier wird Hibernate für das Erzeugen des Datenbankschemas verwendet. Das Feature wird in Spring Boot über die Property spring.jpa.generate-ddl gesteuert. Das genaue Verhalten kann zusätzlich mit der Property spring.jpa.hibernate.ddl-auto beeinflusst werden. Mehr dazu gibt es natürlich in der Spring-Doku.

Da man hierbei wenig bis keine Kontrolle über die tatsächlich durchgeführten Änderungen an der Datenbank hat, ist diese Variante nur für den Einsatz in der Entwicklungsumgebung (z.B. einer InMemory-DB wie H2 HSQL oder Derby) zu empfehlen.

Für produktive Umgebungen empfiehlt es sich ein spezialisiertes Tool zu verwenden, welches einem eine Möglichkeit bietet, die geplanten Schemaänderungen zu prüfen und ggf. anzupassen.

Flyway bietet die Möglichkeit SQL Skripte automatisch beim Start der Anwendung auszuführen. Dabei speichert sich Flyway in einer eigenen Tabelle, welche Skripte bereits ausgeführt wurden. Dadurch werden immer nur die für das Update benötigten Skripte ausgeführt. Die Skripte für die Migration müssen von Hand geschrieben werden. Wer einen Blick riskieren will, kann sich im offiziellen Spring Git-Repository das Beispiel für die Flywy-Integration anschauen.

Liquibase verfolgt einen ähnlichen Ansatz, allerdings basiert es nicht auf fertigen SQL Skripten, sondern auf einem eigenen XML Format. Aus diesem werden dann zur Laufzeit die benötigten Skripte für die verwendete Datenbank generiert. Auch Liquibase verwendete eine Tabelle in der Datenbank um zu tracken, welche Änderungen bereits durchgeführt wurden. Das Integration dieser Möglichkeit schauen wir uns in den kommenden Zeilen genauer an. Viel Spaß!

Konfiguration von Liquibase in Spring Boot

Um Liquibase in Spring Boot zu integrieren, benötigen wir als erste die entsprechenden Libraries im Classpath unserer Anwendung. In unserem Beispiel verwenden wir dafür Maven. Das folgende Paket muss in der pom.xml als Dependency hinzugefügt werden:

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

Alternativen für z.B. Gradle gibt es im Maven Repository.

Als nächstes müssen wir unser Master-Changelog anlegen. Dieses wird als XML Datei in den Ressourcen unserer Anwendung abgelegt.

Master.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/dbchangeloghttp://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">

</databaseChangeLog>

Als letztes müssen wir Spring noch mitteilen wo unser Master-Changelog zu finden ist. Außerdem müssen wir Datenbankinitialisierung von Hibernate auf stellen, damit wir bei Fehlern in unserem Schema zwar gewarnt werden, aber Hibernate nicht versucht das Schema zu updaten.

Für die Konfiguration haben wir YAML verwendet. Anpassungen müssen mit folgenden Properties getätigt werden:

resources/application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: validate
      liquibase:
      change-log: classpath:database/migration/master.xml

Das war’s auch schon. Beim nächsten Start unserer Anwendung sollten wir die folgenden Einträge im Log sehen:

Log

...
2018-03-10 15:49:18.966 INFO 15589 --- [ main] liquibase : Successfully acquired change log lock
2018-03-10 15:49:19.541 INFO 15589 --- [ main] liquibase : Creating database history table with name: PUBLIC.DATABASECHANGELOG
2018-03-10 15:49:19.542 INFO 15589 --- [ main] liquibase : Reading from PUBLIC.DATABASECHANGELOG
2018-03-10 15:49:19.546 INFO 15589 --- [ main] liquibase : Successfully released change log lock
...

Außerdem sollte unsere Datenbank zwei neue Tabellen (DATABASCHANGELOG und DATABASECHANGELOGLOCK) enthalten.

Generierung von Changelogs

Wir könnten jetzt anfangen Changelogs für unsere Anwendung zu schreiben. Allerdings ist viel praktischer diese automatisch aus unseren JPA-Entities generieren zu lassen. Dafür verwenden wir das Liquibase Maven Plugin. Dieses muss folgendermaßen in der pom.xml konfiguriert werden:

<plugin>
  <groupId>org.liquibase</groupId>
  <artifactId>liquibase-maven-plugin</artifactId>
  <version>${liquibase.version}</version>

  <dependencies>
    <dependency>
      <groupId>org.liquibase.ext</groupId>
      <artifactId>liquibase-hibernate5</artifactId>
      <version>3.6</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
        <version>2.0.0.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>javax.validation</groupId>
      <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
    </dependency>
  </dependencies>

  <configuration>
    <changeLogFile>src/main/resources/database/migration/master.xml</changeLogFile>
    <diffChangeLogFile>src/main/resources/database/migration/changelogs/diff_changelog.xml</diffChangeLogFile>

    <driver>org.h2.Driver</driver>
    <url>jdbc:h2:~/migration</url>
    <username>sa</username>

    <referenceUrl>hibernate:spring:de.pragtics.springbootliquibase?dialect=org.hibernate.dialect.H2Dialect&amp; hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&amp; hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy</referenceUrl>
  </configuration>
</plugin>

Damit das Plugin mit JPA-Entities umgehen kann, benötigt es die folgenden Dependencies:

  • liquibase-hibernate5
  • spring-boot-starter-data-jpa
  • validation-api

Im Abschnitt <configuration> müssen die folgenden Einstellungen vorgenommen werden:

<changeLogFile>src/main/resources/database/migration/master.xml</changeLogFile>

Hier müssen wir den Pfad zu unserem Master-Changelog angeben.

<diffChangeLogFile>src/main/resources/database/migration/changelogs/diff_changelog.xml</diffChangeLogFile>

Hier können wir konfigurieren, wo die generierten Changelogs abgelegt werden sollen.

<driver>org.h2.Driver</driver>

Hier geben wir an welchen Datenbanktreiber wir für die Generierung der Changelogs verwenden möchten. In unserem Fall verwenden wir H2

<url>jdbc:h2:~/migration</url>

Das ist der Connection-String zu unserer H2 Datenbank

<username>sa</username>

Und der entsprechende Benutzername

<referenceUrl>hibernate:spring:de.pragtics.springbootliquibase?dialect=org.hibernate.dialect.H2Dialect&amp;
hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&amp;
hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy</referenceUrl>

Hier konfigurieren wir einen Connection-String, welcher auf unsere JPA-Entities verweist. Dieser beginnt mit „hibernate:spring:“ gefolgt von dem Paket welches als Basis für den Scan nach JPA-Entities verwendet werden soll.
Damit alles mit der Standardkonfiguration von Spring funktioniert, müssen wir noch die physical_naming_strategyund implicit_naming_strategy konfigurieren

Damit ist die Konfiguration des Plugins abgeschlossen.

In­i­ti­ales Changelog

Als nächstes müssen wir die aktuelle Version unserer Datenbank in Form eines Changlogs abbilden. Dafür müssen wir die folgenden Schritte durchführen:

1. mvn compile

Da Liquibase auf den kompilierten .class Dateien arbeitet, müssen wir unsere Anwendung bauen, bevor wir Liquibase ausführen können.

2. mvn liquibase:diff

Jetzt wird die Migrationsdatenbank (H2) mit unseren JPA-Entities verglichen, da unsere Migrationsdatenbank noch leer ist, wird ein Changelog für die initiale einrichtung der Datenbank erstellt. Dieses wird an dem Pfad abgelegt, welchen wir über die Property diffChangeLogFile im Plugin konfiguriert haben.

Dieses sollten wir jetzt in z.B. 01-create-initial-schema.xml umbenennen. Danach verlinken wir es als Import mit unserem Master-Changelog. Dafür fügen wir dort die folgenden Zeile ein:

Master.xml

<includefile="changelogs/01-create-initial-schema.xml" relativeToChangelogFile="true"/>

Zum Schluss müssen wir noch unsere existierenden Datenbanken für die Verwendung mit Liquibase vorbereiten. Dafür müssen wir dort das in­i­ti­ale Changelog als bereits Ausgeführt markieren. Dafür führen wir den Befehl mvn liquibase:changelogSyncSQL aus. Dieser generiert uns eine SQL Skript (/target/liquibase/migrate.sql), welches die Datenbank entsprechend vorbereitet. Dieses muss von Hand auf allen existierenden Datenbanken ausgeführt werden bevor Liquibase dort zum Einsatz kommen kann.

Workflow für Änderungen am Schema

Nachdem wir jetzt alles konfiguriert haben, müssen wir nur nach jeder Änderung an unseren JPA-Entities die entsprechenden Changelogs generieren und mit unserem Master-Changelog verlinken.

Der vollständige Workflow sieht dann wie folgt aus.

  1. Anpassung der JPA-Entities
    In unserem Beispiel fügen wir der Entity MyEntity eine neue Property mit dem Namen property3 hinzu.
  2. mvn compile
    Damit die Änderungen auch in den kompilierten Klassen zu finden sind.
  3. mvn liquibase:dropAll
    Damit löschen wir alle Tabellen aus unserer Migrationsdatenbank.
  4. mvn liquibase:update
    Liquibase wird jetzt das komplette Master-Changelog auf unserer Migrationsdatenbank ausführen. Danach sollte das Schema der letzten Version unserer Datenbank entsprechen.
  5. mvn liquibase:diff
    Jetzt wird die Migrationsdatenbank mit unseren JPA-Entities verglichen. Als Ergebnis erzeugt Liquibase ein neues Changelog.

6. Review und Anpassung des Changelogs

Das generierte Changelog sollte unbedingt von Hand geprüft werden. Nur so kann sichergestellt werden, dass beim generieren keine Fehler entstanden sind.

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
  <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
  <changeSet author="Sascha (generated)" id="1520688989576-1">
    <addColumn tableName="my_entity">
      <column name="property3" type="boolean">
        <constraints nullable="false"/>
      </column>
    </addColumn>
  </changeSet>
</databaseChangeLog>

Unser Changelog sieht gut aus. Man erkennt das Liquibase gerne die Spalte property3 zur Tabelle my_entity hinzufügen möchte.

7. Umbenennen des Changelogs

Wir sollten dem generierten Changelog diff_changelog.xml einen sprechenderen Namen geben. Es bietet sich an die Changelogs zu Nummerieren z.B 02-add-property3.xml

Eine weitere Möglichkeit ist es, die Ticket-ID einer Anforderung in den Namen zu integrieren, dadurch lässt sich das Changelog später einer Anforderung zuordnen z.B. 02-PROJECT-143.xml

Da Liquibase sich den Pfad zu unserem Changelog merkt, sollten wir auch einen logicalFilePath setzen. Dadurch verhindern wir später Probleme, falls sich z.B. die Ordnerstruktur unseres Projekts mal ändert.

<databaseChangeLog logicalFilePath="02-add-property3" xmlns="http://www.liquibase.org/xml/ns/dbchangelog" ... >

8. Integration in das Master-Changelog

Das neue Changelog muss jetzt noch in unser Master-Changelog integriert werden. Dafür fügen wir dort die folgende Zeile ein:

<include file="changelogs/02-add-property3.xml" relativeToChangelogFile="true"/>

Damit ist die Änderung erfolgreich als Changelog abgebildet. Wenn wir jetzt die Anwendung starten, sollte Spring keine fehlenden Spalten bemängeln, da Liquibase die Datenbank entsprechend angepasst hat.