Wieso automatisierte DB-Schema-Migrationen?
Hans-Joachim Daniels
7 Minuten
Problemstellung
Ihr entwickelt fleißig eure Anwendung. Aber leider zerschießt ihr sie euch einander mit Änderungen am Datenmodell, sodass eure Mitentwickler:innen das Modell regelmäßig erneut von Hibernate o.ä. mit DROP
und CREATE
neu anlegen lassen müssen und dabei ihre Testdaten verlieren. Testdatenskripte müssen dabei natürlich auch angepasst werden oder es muss herausgefunden werden, was sie von Hand auf der DB ausführen müssen, damit die Anwendung wieder startet.
Und was auf Entwicklungsrechnern ein Problem ist, wird noch größer, wenn es um gemeinsam genutzte Umgebungen geht, wo das Wegwerfen der bislang angesammelten Daten keine Option mehr ist. Hinzu kommt noch, dass händisch ausgeführte Skripte zu fehleranfällig sind und außerdem Einblickmöglichkeiten in Daten schaffen, die evtl. gar nicht freigegeben sind. Wenn nur neue Spalten hinzukommen oder alte wegfallen, kann der Object Relational Mapper (ORM) das Schema evtl. behutsam ändern, aber sobald Werte aus anderen berechnet werden müssen, geht das leider nicht mehr.
Mit Continuous Delivery hat das dann auch nicht mehr viel zu tun, sämtliche Installationsschritte und dazu gehören DB-Schema-Änderungen, sollten automatisiert werden.
Lösung: Automatisierung
Zum Glück ist das oben Beschriebene ein bereits gelöstes Problem.
Die Änderungen am DB-Schema werden als einzelne Migrationsschritte mit festgelegter Reihenfolge geschrieben. Diese Schritte werden über ein gesondertes Werkzeug ausgeführt. Dieses Werkzeug führt in der Zieldatenbank in einer eigenen Tabelle Buch darüber, welche Schritte bereits ausgeführt wurden. In der einfachsten Form ist das die Nummer des zuletzt ausgeführten Schrittes, in größeren Ausbaustufen mit einzelnen Einträgen je Schritt, ggf. auch mit Datum oder gar Prüfsumme.
Die erwähnten Schritte können von Hand geschrieben sein. Das geht in SQL (oder allgemeiner, der jeweiligen Schemaänderungssprache der DB) oder im Programmcode sein, der die DB à la Migrationsbatch aufruft. Alternativ bieten manche Werkzeuge an, die Änderungen aus einem XML-Modell oder den Entitätsklassen eines ORMs zu errechnen und daraus Datenbankbefehle zu erzeugen (die dann bei Bedarf noch angepasst werden können).
Ausführung
Bei Ausführung werden alle ausstehenden Schritte in ihrer definierten Reihenfolge ausgeführt.
Im einfachsten Fall wird das Werkzeug direkt vor dem Start der Anwendung ausgeführt. Dadurch wird sichergestellt, dass die Anwendung die DB so vorfindet, wie sie sie erwartet. Manche Werkzeuge lassen sich in die Anwendung einbinden, sodass dieselben Verbindungsdaten genutzt werden können (Achtung: das Datenbankschema zu ändern erfordert höhere Rechte, als die für den Regelbetrieb nötigen Rechte zum Lesen und Schreiben, es sollten also möglichst getrennte Zugangsdaten verwendet werden).
Alternativ kann das Werkzeug im Startskript der Anwendung oder als initContainer
aufgerufen werden.
Hierbei werden Anwendung und DB-Migration gemeinsam ausgeliefert und passen damit immer zusammen. Solange die Migration sicher klappt, ist das gut und einfach, sollten die DB-Änderungen fehlschlagen, hat man eine nicht-startende Anwendung!
Diese Vorteile greifen aber nur, wenn laufende Migrationsschritte andere Instanzen der Anwendung nicht stören. Im einfachsten Fall dürfen alle Instanzen der Anwendung dafür heruntergefahren werden. Das ist zwar das Gegenteil von “zero downtime deployments”, spart aber eine Menge Entwicklungsarbeit und wenn die Migrationsschritte schnell ablaufen, reicht das je nach Anwendung auch aus. Für höhere Robustheit durch leichtes Zurückrollen können Transaktionen genutzt werden.
Kommt ein Herunterfahren der ganzen Anwendung nicht infrage, muss die DB-Migration nebenläufig zur Anwendung ablaufen. Hierbei muss dann peinlich darauf geachtet werden, dass keine Zwischenstände der DB für die Anwendung sichtbar werden, die für irgendeine gerade laufenden Anwendungsversion unverträglich sind, denn es laufen in diesem Szenario immer mindestens die bisherige Anwendungsversion und die nächsthöhere parallel. Mit den Datenbankänderungen dürfen also nur Möglichkeiten für die Anwendung geschaffen werden, die dann von den folgenden neueren Anwendungsversionen genutzt werden. Datenbankänderungen dürfen keine geändertes Anwendungsverhalten erzwingen. In der folgenden Anwendungsversion muss dann darauf geachtet werden, dass diese Möglichkeiten auch durchgängig genutzt werden und die anfangs noch laufende alte Programmversion nicht gestört wird. Nebenläufig müssen die Bestandsdaten flächendeckend angepasst/umkopiert werden. Beides ist nötig, da neue Programmversionen nur gerade eh beschriebene Daten anfassen, Migrationsjobs aber nur diejenigen Datensätze ändern, die es schon gibt und nicht die kurz darauf erst eintreffenden.
Da Code und DB-Schema hierbei nicht zusammen ausgeliefert werden, muss anderweitig darauf geachtet werden, dass nur passende Stände auf einer Umgebung zusammentreffen.
Umsetzungstipp: Bei gleichem Tag in der Versionsverwaltung haben die Stände zusammenzupassen. Wenn statt Tags Zweige wie main
eingespielt werden, muss der gerade eingecheckte Teil noch zum main
-Stand des anderen Teils passen und dann direkt eingespielt werden.
Bei Problemen nach dem Einspielen
Leider können, trotz Tests, manchmal Probleme mit der zuletzt eingespielten Schema-Version auftreten. Diese Probleme können mehrerlei Ursachen haben.
Es gilt, dafür zu sorgen, dass abgebrochene, teilweise ausgeführte Migrationen die DB nicht in einem kaputten Zustand hinterlassen. Läuft das Skript in einer Transaktion, reicht es, zu warten bis der automatische Rollback durch ist. Ist absehbar, dass die Migration oder ihr Zurückrollen zu lange die DB sperren (oder sind Transaktionen von der DB gar nicht unterstützt), dann sollte man darauf achten, alle Einzelschritte reversibel zu halten und z.B. den Inhalt gelöschter Spalten erst in einer Abstell-Tabelle zu parken und ganz am Schluss zu löschen. Oder erst später, wenn sichergestellt ist, dass man den neuen DB-Stand auf Produktion auch wirklich behalten will. Falls man merkt, dass die Abbruchursache nicht in der Migration selber lag, lässt sich so ein Zwischenzustand auch von Hand weiter gen Abschluss treiben.
Ist der Migrationsschritt erfolgreich durchgelaufen, aber harmoniert nicht mit der Anwendung, gilt er herauszufinden, welcher Teil (Anwendung oder DB-Schema) einfacher, schneller und mit möglichst geringem Risiko, die Lage nicht zu verschlimmbessern, anzupassen ist, um die Kuh vom Eis zu bekommen.
Manche Migrationswerkzeuge (z.B. MyBatis Migrations oder Flyway in der Bezahlversion) bieten Rückwärts- oder Undo-Migrationen an, die von Hand angestoßen werden müssen, aber direkt auch den Versionszähler der DB zurücksetzen. Leider sind diese kein Allheilmittel:
-
wenn Daten bei der Migration gelöscht wurden, kann keine Undo-Migration sie wiederherstellen. Wenn man das will, muss man die oben erwähnten Abstell-Tabellen länger aufbewahren und im Undo-Skript verwenden
-
wenn eine Datenkonstellation auf Produktion beim Schreiben des Migrationsskriptes übersehen wurde, dann wird das Undo-Skript auch keine Rücksicht darauf nehmen
-
Wenn man sich auf Rückwärtsmigrationen verlassen will, dann müssen diese genauso gut getestet sein, wie ihre Vorwärtsgeschwister
-
Auch wenn ein Werkzeug keine Undo-Skripte unterstützt, kann man diese trotzdem Schreiben und dann gänzlich von Hand ausführen (einschließlich Versionsbuchführung). Nicht toll, aber wenn die Lage toll wäre, bräuchte man das Undo nicht (wenn man nicht gerade auf einer Testumgebung zwischen zwei Ständen wechselt, wofür ein Werkzeug-unterstütztes Undo in der Tat sehr praktisch ist)
tl;dr
DB-Migrationswerkzeuge helfen, dass Anwendungscode und DB-Schema entweder ganz ohne menschliches Zutun zusammenpassen oder, bei nebenläufigem Einspielen, nur die richtigen Versionen ausgewählt werden müssen. Von Hand muss keiner mehr fehlerträchtig direkt auf die DB (und dabei nebenbei etwa noch Daten abgreifen).
Im Gegensatz zu ORMs mit automatischer Schema-Erzeugung werden vorhandene Daten dabei auf vom Entwickler vorgesehene Weise migriert. Nur automatisiert einspielbare DB-Migrationen passen in eine CI/CD-Welt.
Konkrete Werkzeuge
- Open Source
- auch ohne MyBatis nutzbar
- in Skripten nur SQL unterstützt
- Vorwärts- und Rückwärtsmigrationen
- übersprungene Migrationen können nachgeholt werden
- in Java geschrieben (jedoch keine offizielle Integration in Spring Boot)
- Open Core, zusätzliche Features müssen kommerziell lizenziert werden
- Basisfunktionalität Open Source
- in Java&Groovy geschrieben, gute Integration z.B. in Spring Boot
- Open Core, viele Features, z.B. Rückwärtsmigrationen oder weitere Datenbanken müssen kommerziell lizenziert werden
- Basisfunktionalität Open Source
- in Java geschrieben, gute Integration z.B. in Spring Boot
- Open Source
- in golang geschrieben, kompakte Binärdatei
- lässt sich gut als eigenständiges Programm/initContainer ausführen, Integration als Bibliothek in Go-Programme möglich
- viele unterstützte Datenbanken
Migrationen mit Entity Framework
- Open Source
- für .NET
- Unterstützung verschiedener SQL-Datenbanken
Diesen Artikel teilen über: