Datenbankversionierung – Vor- und Nachteile bestimmter Praktiken

Gerade hatte ich mal wieder ein Schlüsselerlebnis in meiner Programmierung. In diesem Fall gab es ein Kollision mit YAML (YAML Ain’t Markup Language) – ich spreche explizit von Kollision, denn meine Erfahrung ist für den verwendeten Anwendungsfall einfach nur schlecht bzw. ich sehe leider aktuell einige Nachteile.

Datenbank-Schemata versionieren

Zur Versionierung von Datenbank-Änderungen bzw. um den Weg von einer “leeren” Datenbank bis hin zum aktuellen Stand der Entwicklung zu beschreiben verwendet man in der Regel entsprechende Tools wie Liquibase oder Flyway. Das Prinzip ist im Prinzip immer das Gleiche – mit ein wenig “Rahmenwerk” drum herum steuert man die Ausführung der Datenbank-Statements. Dabei kann man auf verschiedene Szenarien eingehen, einer der häufigst genannten ist, dass man ein Produkt mit verschiedenen Datenbank-Servern laufen lassen kann. Das ist beispielsweise wichtig wenn man Software an Firmen (seltener private Anwender) ausliefert, sich aber darauf einstellen muss, dass dort nicht unbedingt die aktuelle Version von MySQL läuft sondern eben im Zweifel auch ein PostgreSQL oder eine andere (kommerzielle) Datenbank wie Oracle.

Selbst wenn man den Bedarf für unterschiedliche Datenbanken nicht hat, ist es hilfreich wenn man Schema-Änderungen im Rahmen eines Deployments automatisch mit ausrollen kann, genau hierbei unterstützen diese Werkzeuge. Mit Liquibase wird Protokoll geführt welche Änderung bereits im System eingetragen ist und daher nicht mehr ausgeführt werden muss. Denn eine Tabelle zweimal anlegen ist eben nicht möglich, genausowenig wie das Anlegen bereits bestehender Spalten in der gleichen Tabelle.

Flexibilität in allen Richtungen

Liquibase selbst ist dabei eine Art “schweizer Taschenmesser” – es versteht sich sowohl auf der Datenbankseite als auch bei der Beschreibungssprache auf unterschiedlichste Formate. Das ist hilfreich um den Entwicklern den Übergang zu einer weiteren Syntax zu ersparen – macht man das meiste ohnehin in XML, so kann man auch Liquibase-Changes in XML-Files definieren, verwendet man lieber JSON nehme man das. Natürlich gibt mittlerweile auch das “hippe” YAML-Format, das sich in vielen Fällen recht gut eignet.

Die Flexibilität hat (wie so häufig) aber leider auch ihren Preis – manche verkaufen das auch noch als Vorteil, ich sehe darin keinen: Man kann jeglichen Ausdruck eines SQL-Statements auch durch eine Abfolge von YAML, JSON oder XML-Statements ausdrücken.

Das SQL das wir ausführen möchten legt einfach nur eine recht überschaubare Tabelle an, definiert dazu einige Spalten und einen Primärschlüssel auf die id-Spalte (welche MySQL-spezifisch eine autoincrement-Spalte) ist.

CREATE TABLE `my_schema`.`some_table` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`comment` TEXT NOT NULL,
`date` DATETIME NOT NULL
PRIMARY KEY (`id`)
)
COLLATE='utf8_general_ci' ENGINE=InnoDB;

Wenn mann das ganze nun in ein einfaches Liquibase-Changeset mit XML-Syntax verpackt dann sieht es ungefähr so aus:

<?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-2.0.xsd">

    <changeSet id="20201703" author="some_developer">
        <sql>
          <![CDATA[
          CREATE TABLE `my_schema`.`some_table` (
              `id` INT(11) NOT NULL AUTO_INCREMENT,
              `username` VARCHAR(50) NOT NULL,
              `comment` TEXT NOT NULL,
              `date` DATETIME NOT NULL
              PRIMARY KEY (`id`)
             )
          COLLATE='utf8_general_ci' ENGINE=InnoDB;
        ]]>
</sql>
</changeSet>
</changelog>

Wie man sieht habe ich hierbei einfach mal das SQL nativ “reinkopiert” und die entsprechenden Tags drum herum gesetzt. Die “CDATA-Angaben” haben sich als hilfreich erwiesen, da XML-Parser sonst gerne meinen sie müssten das SQL interpretieren, was ihnen meist nicht schmeckt bzw. nicht gut bekommt.

Weiter abstrahieren

Nun gibt es auch noch weitere Möglichkeiten mit Liquibase, man ist nicht darauf angewiesen in XML zu schreiben, ebenso gibt es mittlerweile die Möglichkeit YAML oder auch JSON zur Definition zu verwenden. Zusätzlich ist es keine Pflicht die Statements wie oben in nativem SQL zu schreiben. Das ist insbesondere dann gefragt, wenn man mit verschiedenen Datenbank-Systemen zurecht kommen muss. Denn auch wenn MySQL und MariaDB recht weit verbreitet sind und vielen Entwicklern ein Begriff sind, so haben auch diese Systeme eine Geschichte und somit auch Eigenheiten und Spezialitäten entwickelt. Klassische Beispiele hierfür sind die ENUM-Spalten oder auch die Auto-Increment-Spalten. Andere Datenbanksysteme wie DB2, Oracle oder Postgres kennen diese “Dialekte” nicht und haben dafür andere “Eigenzüchtungen” im Angebot. Man müsste sich daher auf einen gewissen Standard beschränken und selbst dann ist man ggf. nicht ganz auf der sicheren Seite, denn nicht jeder Standard ist zu 100% gleich umgesetzt. Formuliert man etwas derartiges jetzt in YAML so erhält man folgendes

databaseChangeLog:
  - changeSet:
      id: 20201703
      author: somedeveloper
      changes:
        - createTable:
            columns:
              - column:
                  autoIncrement: true
                  constraints:
                    nullable: false
                    primaryKey: true
                  name: id
                  type: INT
              - column:
                  constraints:
                    nullable: false
                  name: username
                  type: VARCHAR(50)
              - column:
                  name: comment
                  type: TEXT
              - column:
                  constraints:
                    nullable: false
                  name: date
                  type: datetime
            tableName: some_table
            schemaName: my_schema

Das sieht auf den ersten Blick noch ganz vernünftig aus, aber man merkt schon: vom einem kurzen SQL-Skript welches eine Tabelle anlegt sind wir jetzt doch ein ganzes Stück weit weg und ich habe hier jetzt nur eine vergleichsweise einfache Tabelle mit wenigen Spalten als Beispiel heran gezogen. Wenn es komplexer wird, steigt die Länge der Datei recht schnell weiter an und die Übersichtlichkeit ist recht schnell dahin. Wie auch auffällt ist das Schema und der Tabellenname am Ende angeordnet was ich persönlich nicht geschickt finde – gut dass YAML es erlaubt den Anteil auch weiter oben zu definieren. Leider ist das nicht der Standard wenn man sich helfen lässt und auf ein bestehendes Schema Liquibase zum erzeugen der Changesets heran zieht.

liquibase --driver=com.mysql.jdbc.Driver --url="jdbc:mysql://127.0.0.1:3306/my_schema?autoReconnect=true&useSSL=false" --username=my_user --password=my_secret --changeLogFile="my_changes.yaml" --classpath=/usr/share/java/mysql-connector-java-8.0.19.jar generateChangeLog

Interessant an dieser Stelle sind folgende Punkte:

  • Liquibase ist ein Java-Programm, dementsprechend muss man ihm (wie so häufig) alles mögliche per zusätzlichem Parameter bekannt geben – einfaches und robustes Handling ist das nicht unbedingt
  • Liquibase “rät” welchen Changelog-Typ man haben möchte anhand des Dateinamens den man angibt

Flexibilität durch Virtualisierung (Docker)

Flexibilität ist schön und gut, aber wie wir gesehen haben, hat sie auch einige Nachteile. Es gilt daher abzuwägen ob man diese wirklich benötigt, zumal es bestimmte Einsatzbereiche gibt, in denen eine bestimmte Datenbank-Engine einfach erhebliche Vorteile bietet (sei es technisch, sei es dass spezielle Analysefunktionen vorhanden sind, sei es aus Kostengründen). Mit der Flexibilität durch Liquibase schränken wir die Optimierungsmöglichkeiten stellenweise etwas ein (Flexibilität gibt es eben doch nicht zum Nulltarif).

Einige Projekte gehen daher mittlerweile einen etwas anderen Weg und schreiben die Nutzung einer bestimmten Datenbank (wieder) vor. Im ersten Moment vielleicht ärgerlich, aber aus Sicht des Entwicklungsteams auch nachvollziehbar: Wenn man nur noch eine oder zumindest nur ein sehr ähnliches Subset an Datenbanken anbinden muss, kann man sich voll darauf konzentrieren.

Werfen wir daher nochmal einen Blick zurück, weshalb man eigentlich verschiedene Datenbank-Systeme unterstützen möchte/muss: Meist möchte man es so vielen Kunden / Nutzern der eigenen Software so einfach wie möglich machen. Ein Shopsystem erfährt eine größere Akzeptanz wenn man zusätzlich zur eigentlichen Software nicht auch noch einen Datenbank-Server aufsetzen muss (z.B. weil man ohnehin schon eine Server-Instanz oder gar einen Cluster am Laufen hat) – das senkt die Administrationsaufwände und somit die Betriebskosten. Das Konzept ist vergleichsweise alt: geteilte Ressourcen kann man besser auslasten, zum Beispiel die ohnehin vorhandene Datenbank-Server-Lizenz oder den Server auf dem die Datenbankinstanz läuft.

Allerdings muss man sich gerade heute fragen ob dieser Ansatz noch in allen Fällen aktuell ist: War es früher einfach nur mühsam und fehleranfällig mehrere Datenbank-Systeme auf einem Host laufen zu lassen (womöglich sogar mit diametralen Anforderungen an das Betriebssystem – die eine Datenbank funktioniert am einfachsten unter RedHat Enterprise, die andere ist eher unter Ubuntu daheim …) so haben wir heute doch recht ausgefeilte Tools wie Docker um zu Virtualisieren. Auch hier gibt es kontroverse Diskussionen ob man Datenbanken (welche ja der Persistenz verpflichtet sind) in Container (welche eigentlich “stateless” sein sollten) zu packen. Auch hier gilt: es gibt nicht “die Lösung” – man sollte immer das Umfeld in Betracht ziehen in welchem man gerade arbeitet.

Mit Docker oder sogar Software as a Service (SaaS) ist es ja doch deutlich leichter geworden multiple Datenbank-Systeme für den jeweiligen Anwendungsfall zu nutzen. Einen gewissen zusätzlichen Aufwand hat man aber dennoch.

Fazit

Generell ist die Nutzung einer Versionskontrolle für Datenbanken eine feine Sache – welche exakte Lösung man nun bevorzugt bzw. in einem Projekt bereits eingesetzt wird ist eher eine Geschmacksfrage. So lange man das Prinzip verstanden hat kommt man mit jeder Lösung ans Ziel.

Wenn man den verwendeten Datenbanktyp nicht unter Kontrolle hat (oder es bewusst offen lassen möchte) so ist die abstrakte Syntax in Liquibase sicherlich hilfreich.

Hat man hingegen die Datenbank unter Kontrolle (wie es bei “Eigengewächsen” sehr häufig der Fall ist) so spricht vieles dafür die Möglichkeiten der Datenbank auch auszunutzen und potentiell eher natives SQL in die Changesets zu packen. Zur Überwachung und Entwicklung kann man dann ohne Bedenken externe Tools wie z.B. HeidiSQL oder MySQL Workbench einsetzen, mit dieser grafischen Unterstützung ist das Erstellen von Tabellen und Fremdschlüsseln einfach wesentlich angenehmer und weniger fehlerträchtig als wenn man SQL oder die anderen Dialekte von Hand schreiben muss. Noch dazu ist es im Fehlerfall ein leichtes einfach einmal den SQL-Schnippsel zu schnappen und auf einer Maschine manuell laufen zu lassen um den Fehler nachspüren zu können.

Zum Abschluss komme ich noch auf die Feinheit zurück, was die Schreibweise der Änderungen betrifft. Einige sprechen beim Verzicht von reinen SQL-Schnippseln auch von “nativen” Liquibase, ein Begriff den ich so nicht nachvollziehen kann, denn ist dann die XML-Notation nativer als die YAML oder JSON-Variante? Sofern die Notwendigkeit mehrerer Datenbanksysteme besteht dann ist die Abstraktion zwingend notwendig, ist das nicht der Fall so sollte man den SQL-Statements den Vorzug geben eine vergleichsweise gut eingespielte Sprache wie SQL wird nicht präziser wenn man sie nochmal weiter abstrahiert und damit vom eigentlichen Problem entfernt um sie hinterher wieder in SQL zu transformieren.