Grails Migrations Plugin

Motivation

Oft passiert es, dass bei einem Deployment Aufgaben anstehen, die einmalig ausgeführt werden sollen und dann nie wieder. Beispielsweise die Änderung von Datenbankwerten, hinzufügen von Kategorien im Online-Shop, programmatische Änderung der Produktpreise anhand eines Algorithmus, usw.

Habe ich eine Administrationsoberfläche, mit der ich diese oder jene CRUD Aufgaben lösen kann, ist es einfacher. Oft bleibt aber nicht die Zeit eine aufwendige Backend-Oberfläche zu schreiben.

Deshalb habe ich das Migrations Plugin geschrieben. Es ist primitiv, aber hilfreich. Die Idee kam mir schon, als ich Aufgrund meiner Arbeit von Ruby on Rails zu Grails gewechselt bin. Denn Rails kennt die Rails Migrations.

Eine Reihe, nach dem Zeitstempel sortierter Skripte, die eine up() und down() Methode kennen. Wird eine Anwendung zum ersten mal deployed, werden die up() Methoden der Skripte nacheinander ausgeführt und der aktuelle Versionsstand in der DB gespeichert. Bei einem erneuten Deployment, geht es bei der Version weiter, an der für diese Anwendung beim letzten Deployment Schluss war.

Bei Ruby on Rails sind die Migrations noch wesentlich notwendiger, als bei Grails. Denn Hibernate kennt das Hbm2DDL Feature, mit dem sich das Datenbankschema aus der Definition der Entitäten automatisch erzeugen lässt, was es so bei Rails nicht gibt.

Funktionsweise

Wird das Plugin installiert, erstellt es das ${basedir}/grails-app/migration Verzeichnis, in dem die Migrationen erwartet werden. Als eine Migrationsklasse werden alle Klassen verstanden, die sich in diesem Verzeichnis befinden und mit „Migration“ enden.

z.B. sieht eine Migration so aus:

class V100CountryMigration {
  def version = 100
 
  def up = {
    new Country(code: 'de', name: "Deutschland", defaultCountry: true).save()
  }
 
  def down = {
    Country.findByCode("de")?.delete()
  }
}

Man sieht bereits an diesem Beispiel alle drei Bestandteile einer Migrationsklasse:

  1. version Bestimmt die Version der Migration. Muss eine positive, eindeutige Zahl sein. Darf auch nicht fehlen.
  2. up() Dieses Closure wird aufgerufen, wenn die Migration durchgeführt werden soll. Optional
  3. down() Dieses Closure wird aufgerufen, wenn die Migration rückgängig gemacht werden soll. Optional

Ob die Migration durchgeführt werden soll, oder nicht, kann über die Konfiguration bestimmt werden:

migrations {
  // Migrate the application on startup
  onStartup = true
}
 
environments {
  test {
    migrations {
      // Don't migration application during the test phase
      onStartup = false
    }
  }
}

Die manuelle Integration kann über den MigrationService erfolgen, der zwei Methoden kennt:

  1. up() Migriert die Anwendung
  2. down() Macht die Migration Rückgängig.

Tipps

Die Migrationen eigenen sich für bestehende Datenbestände und auch für frisch-deployte Anwendungen. Das liegt daran, dass nach jeder erfolgreich durchgeführten Migration sich die Anwendung merkt, welche Migrationen durchgeführt wurden und welche nicht.

Wenn ich Versionen vergebe, dann mache ich das in zehner-Schritten. Also 10, 20, 30, usw. Einfach, um mal Platz für zukünftige Migrationen zu haben, die zwischen zwei bereits bestehenden Versionen „geschoben“ werden sollen. Man weis ja nie.

Die Migrationen laufen übrigens transaktional und werden alle rückgängig gemacht, wenn eine Exception auftritt. Das stellt sicher, dass sich keine Müll in der Datenbank befindet. Führt eine Migrationsklasse Änderungen am Dateisystem durch, bleiben sie natürlich.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.