Datenbank-Migrationen mit Grails

von Tobias Kraft

Problemstellung

Wird für eine Applikation, die darunterliegende Datenbank ausgetauscht, dann sind im Idealfall keine Änderungen im Anwendungscode notwendig. Dies sollte zumindest gelten wenn die Applikation Standard-SQL einsetzt. Unabhängig davon müssen aber die Daten von der ursprünglichen Datenbank in die Zieldatenbank überspielt werden. Für eine Grails-Applikation standen wir genau vor dieser Herausforderung, wobei von einer MySQL-Datenbank auf eine PostgreSQL-Datenbank migriert wurde. 

Hierfür gibt es nun mehrere Möglichkeiten. Relativ einfach erscheint das Durchführen eines Datenbank-Exports, anschließendes modifizieren der Daten und abschließendes Importieren in PostgreSQL. Leider hat bei keinem der verfügbaren Tools zur Modifizierung (siehe [1]) der Import funktioniert, d.h. die Daten konnten nicht korrekt für PostgreSQL modifiziert werden. Des Weiteren gibt es kommerzielle Tools wie bspw. easyfrom, die über eine entsprechende Oberfläche die Migration von der Quell- zur Zieldatenbank ermöglichen. Eine weitere Möglichkeit ist die Verwendung eines Grails-Plugins wie beispielsweise DB Stuff [2]. Mit dem Plugin können Daten aus einer DB in XML abgespeichert werden und von XML wiederum in eine Datenbank eingelesen werden. 

In unserem Fall traten Probleme bezüglich der unterschiedlichen Datentypen zwischen MySql und PostgreSQL auf. Außerdem ist das Handling von DB-Relationen mit dieser Art von Plugins eher schwierig bzw. nicht möglich. Aufgrund der Historie hatten wir auch einige Spalten im Originalschema, für die es kein Mapping mehr in den Grails-Domain-Klassen gab. Sie waren also unbenutzt und sollten möglichst automatisiert entfernt werden. 
Der Entschluss fiel schließlich zur Erstellung einer eigenen kleinen Migrationssuite. 

Migrationsidee

Die Idee zur Durchführung ist relativ einfach. Über Grails Domain-Klassen werden die Daten aus der alten DB ausgelesen und anschließend werden die Inhalte der Domain-Objekte direkt in die neue Datenbank gespeichert. 
Folgende Punkte sind hierbei zu beachten und müssen gelöst werden:

  • Zugriff auf zwei Datenbanken in einer Grails-Applikation
  • Transaktionen: Abhängig von der Datenmenge ist ein entsprechendes Transaktionshandling notwendig
  • Definition von fachlichen Schlüsseln, um Relationen in der Ziel-Datenbank wieder zu erstellen

Migrationsdurchführung

Für den Zugriff auf zwei Datenbanken wurde das Datasources Plugin [3] eingesetzt. Die Standard-Konfiguration für Datenbanken in der Datei DataSource.groovy zeigte auf die alte DB und war wie bisher mit allen bestehenden Domain-Klassen verknüpft. 
Die bestehenden Domain-Klassen wurden alle in ein neues Package kopiert und über das Datasources Plugin mit der neuen DB verknüpft. Hierfür können, wie nachfolgend dargestellt, in der Konfigurations-Datei des Plugins die zu verknüpfenden Domain-Klassen angegeben werden. Des Weiteren kann auch angegeben werden welche Services per Default auf die konfigurierte Data-Quelle zugreifen.

datasources = {
 datasource(name: 'dsPostgresMigration') {
  domainClasses([com.exensio.timesheet.migrate.RenderReportLevel,...])
  readOnly(false)
  pooled(true)
  dialect(org.hibernate.dialect.PostgreSQLDialect)
  driverClassName('org.postgresql.Driver')
  url('jdbc:postgresql://192.168.11.211:5432/extime')
  username('...')
  password('...')
  dbCreate('update') 
  pooled(true)
  services(['migration'])
 }
}

Für die eigentliche Migration ist der MigrationService zuständig. Der Service enthält für jedes zu migrierende Domain-Klassen-Geflecht eine Methode. Im einfachsten Fall führt eine Methode die Migration für genau eine Domain-Klasse und damit Datenbanktabelle durch. Das nachfolgende Code-Fragment zeigt wie im ersten Schritt alle zu migrierenden Domain-Objekte ausgelesen werden. Anschließend wird über diese iteriert und bei jedem Schleifendurchlauf alle Attribute bis auf die technische ID vom alten Domain-Objekt in ein neu erstelltes Domain-Objekt kopiert und gespeichert.

def migrateRenderReportLevel() {
 def renderReportLevels = com.exensio.timesheet.RenderReportLevel.list()
 def domainClass = new DefaultGrailsDomainClass(RenderReportLevel.class)

 renderReportLevels.each {oldDomainObject ->
  def newDomainMap = [:]
  domainClass.persistentProperties.each { property ->
   newDomainMap[property.name] = oldDomainObject[property.name]
  }
  RenderReportLevel newDomainObject = new RenderReportLevel(newDomainMap)
  newDomainObject.save(flush: true)
  if (newDomainObject.hasErrors()) {
   handleErrors(newDomainObject)
  }
  else {
   log.info "${newDomainObject.class.simpleName} ${oldDomainObject.id} migrated"
  }
 }
}

Durch die Nutzung eines Service und die Aufspaltung in mehrere Methoden kann der Ablauf der Migration einfach in mehrere Transaktionen eingeteilt werden. Die Hauptschwierigkeit bei der Migration ist die Auflösung der Beziehungen. In vielen Fällen kann die Migration über mehrere Objekte in einer Methode direkt erfolgen. Allerdings gibt es auch Domain-Klassen, die von mehreren anderen referenziert werden. Hier ist darauf zu achten, dass die Migration nur einmal erfolgt und die Beziehungen anschließend über fachliche Schlüssel erstellt werden. 
In unserem Fall gibt es die beiden Domain-Klassen User und Role, die in einer Methode migriert wurden. Die Klasse User wird auch von der Klasse Project referenziert, die wiederum in einer separaten Methode migriert wurde. Diese Migration-Methode sucht sich über den eindeutigen Login-Namen den entsprechenden User aus der Tabelle und setzt die Beziehung.

Employee existing = Employee.findByUsername(oldDomainObject.projectManager.username)
if (existing) {
 newDomainObject.projectManager = existing
}

Zusammenfassung

Das vorgestellte Verfahren für eine Datenbankmigration erfordert zwar etwas Programmierarbeit, ist aber dank Grails in kurzer Zeit umzusetzen. Es bietet sich insbesondere an wenn bei der Migration Sonderfälle zu beachten sind, wie bspw. das Entfernen nicht mehr benötigter Spalten.

Kategorien: Grails

Zurück