Die neue Klasse zur Ausführung der Migration wird im Unterordner Migrations\[Assembly-Version] erstellt:

Migration Folder

Die Assembly-Version entnimmt man der Datei AssemblyInfo.cs im selben Projekt:

[assembly: AssemblyFileVersion("0.9.0.0")]

Es sind nur die ersten beiden Zahlen relevant. Die Version bei der Migration darf nicht geändert werden, wenn das Assembly auf eine neue Version gehoben wird. Es ist immer die Versionsnummer zum Zeitpunkt der Erstellung.

Die Klasse sollte sich die nächste freie Migrationsnummer im selben Unterordner nehmen. Die Zählung beginnt mit jeder Versionsnummer von vorne.
Die neue Klasse jetzt mit einem Migration-Script-Attribut versehen:

[MigrationScript("0.9", 2)]

Darin muss der Versionsstring der Assembly-Version von eben entsprechen und die folgende Nummer ist die laufende Nummer der Migration.

Normale Migration (nur Datenbank)

Datenbank-Migrationen sind all jene Migrationen, bei denen der Inhalt von Konfigurationsdaten unverändert bleibt, also nichts, was im Konfiguration-ZIP stehen könnte:

  • keine Änderungen an OrmConfigration
  • keine Änderungen an Enums
  • keine Änderungen an Übersetzungen
  • keine Änderungen an Rollen

Normale Migrationsklassen erben von der Klasse MigrationScriptBase.

[MigrationScript("0.9", 2)]
public class Migration_02_RemoveDataList : MigrationScriptBase
{ … }

Eine Datenbank-Migration kann einen oder mehrere Migrationsschritte implementieren.

Es wird eine neue Instanz für jeden Migrationsschritt erstellt, auch, wenn eine Klasse mehrere Migrationstypen implementieren sollte. Es ist nicht möglich, sich innerhalb der Klasse Daten zwischen den Migrationsschritten zu merken! Dazwischen kann ein Anwendungsneustart liegen.

Manuelle Datenbankmigration (BeforeStart)

Dieser Bereich sollte explizit nur noch dazu verwendet werden, manuell notwendige Anpassungen an der Datenbankstruktur vorzunehmen. Das sollten ausschließlich Änderungen sein, die einen Start von DevExpress XPO verhindern.

Dafür sind ausschließlich direkte Datenbankbefehle zulässig. Über die Klasse SQLHelper kann und sollte das einigermaßen datenbankunabhängig geschehen. Der SQLHelper in der Basisklasse implementiert bereits die gängigen Hilfsmethoden, die hier zum Einsatz kommen dürften.

Es obliegt der jeweiligen Migrationsklasse, mit allen unterstützten Datenbanken klar zu kommen.

public override bool ExecuteTheScriptBeforeStart(MigrationExecutor executionContext)
{
    SQLHelper sql = SQLHelper.GetSQLHelper(executionContext.UnitOfWork);
    …
}

Die Methode gibt true oder false zurück, je nachdem ob tatsächlich Änderungen vorgenommen wurden.

Der executionContext enthält unter anderem eine UnitOfWork, die für die Änderungen verwendet werden kann. Es handelt sich um eine ExplicitUnitOfWork, die bis auf Datenbankebene durchgreift. Das System führt automatisch ein Commit durch.

Nach Schema-Update (AfterSchemaUpdate)

Änderungen, die den Start von XPO nicht verhindern, wohl aber den von BA gehören in diese Kategorie. Das sind allen voran Änderungen an Auswahlliste oder Übersetzungen.

Dieser Migrationstyp ist der erste, in dem es erlaubt ist, tatsächliche Daten (Verkehrsdaten sowie Daten für den Betrieb wie Auswahllisten, etc.), die in der Datenbank liegen, per SQL (oder Helper) zu migrieren.

In diesem Bereich kann bereits eingeschränkt mit den Mitteln von XPO gearbeitet werden. Es muss allerdings klar sein, dass die Konfigurationen noch nicht geladen sind. Das bedeutet, dass nicht auf konfigurierbare Entitäten zugegriffen werden sollte.

public override bool ExecuteTheScriptAfterSchemaUpdate(MigrationExecutor executionContext)
{  …}

Die Methode gibt true oder false zurück, je nachdem ob tatsächlich Änderungen vorgenommen wurden.

Der executionContext enthält unter anderem eine UnitOfWork, die für die Änderungen verwendet werden kann. Es handelt sich um eine ExplicitUnitOfWork, die bis auf Datenbankebene durchgreift. Das System führt automatisch ein Commit durch.

Nach dem Start (AfterStart)

Der letzte Migrationsschritt dient der Datenmigration per XPO. Hier sind alle die Änderungen vorzunehmen, die den Start der Anwendung nicht verhindern. Dazu zählt auch das Entfernen nicht mehr benötigter Datenbankspalten.

public override bool ExecuteTheScriptAfterStart(MigrationExecutor executionContext)
{  … }

Die Methode gibt true oder false zurück, je nachdem ob tatsächlich Änderungen vorgenommen wurden.

Der executionContext enthält unter anderem eine UnitOfWork, die für die Änderungen verwendet werden kann. Es handelt sich nicht um eine ExplicitUnitOfWork, sondern nur um eine normale UnitOfWork. SQL-Befehle würde an dieser vorbei arbeiten und sollten hier nicht mehr verwendet werden. Ausnahme: Das Entfernen nicht mehr benötigter Datenbankspalten oder Tabellen.

Konfigurations-Migration

Migrationen, die Objekte verändern, die auch Teil des Konfigurations-ZIPs sind, sollten die Basisklasse ConfigurationMigrationBase nutzen. Darin gibt es eine API, die das verändern von Datenbank-Konfigurationen und Konfigurationen in einem gerade importierten ZIP gleichermaßen erlaubt.

public class Migration_01_RemoveOrphanedRelationType : ConfigurationMigrationBase
{
    public Migration_01_RemoveOrphanedRelationType()  : base(EnumConfigurationType.RelationConfiguration)  { }
    protected override void MigrateConfiguration(EnumConfigurationType type, Guid id, ref string xml)
    { … }
}

Es muss dabei im Konstruktor für die Basisklasse angegeben werden, welche Typen von Konfigurationen man migrieren möchte. Nur diese werden an die MigrateConfiguration Methode durchgereicht.

Die Methode MigrateConfiguration wird für jede einzelne, potentiell zu migrierende Konfiguration einzeln aufgerufen. Dabei wird Typ und ID der Konfiguration zur einfacheren Verarbeitung gleich mitgegeben.

Die Konfiguration wird dabei als XML-String übergeben, der beliebig verändert werden darf. Die Bearbeitung kann mit beliebigen Werkzeugen erfolgen, z.B. LINQ to XML oder DOM.
Wenn der String auf null gesetzt wird, wird die betreffende Konfiguration ersatzlos gelöscht.

Falls für die Migration einer Konfiguration der Inhalt einer anderen Konfiguration erforderlich ist, kann die Methode GetOtherConfiguration der Basisklasse genutzt werden, um diese zu lesen.

Gemischte Migrationen

Migrationen können sowohl Konfigurations- als auch normale Migrationsanteile enthalten. Das ist sinnvoll, wenn die Konfigurationsänderungen auch Änderungen an den Daten erfordern. In diesem Fall ist wie bei einer Konfigurationsmigration zu verfahren und zusätzlich die entsprechenden Methoden für normale Migrationen zu überschreiben.

Manuelle Konfigurationsmigrationen

Einige Migrationen können nicht über die vereinfachte API für Konfigurationsmigrationen durchgeführt werden. Dazu zählen:

  • Migration von Auswahllisten
  • Migration von Rollen
  • Migration von Übersetzungen
  • Anlegen neuer Konfigurationen durch die Migration

In diesen Fällen ist die Migration zweimal zu implementieren (und zu testen!), einmal für die Datenbank durch überschreiben der Methode ExecuteTheScriptForConfiguration und einmal für das ZIP durch überschreiben der Methode ExecuteConfigurationImportMigration.

Migration von Migrationen

Beim Einfügen neuer Migrationen kann es erforderlich sein, den Code älterer Migrationen anzupassen. Im Allgemeinen müssen dabei aber nur Migrationen angepasst werden, die nach der neuen Migration laufen. Das sind Migrationen die spätere Migrationsschritte enthalten als die der neuen Migration.

Beispielsweise kann eine neue BeforeStart Migration die Anpassung existierender AfterSchemaUpdate Migrationen erfordern, da per XPO immer nur in einem Schlag auf das aktuellste Datenbank-Schema migriert werden kann.