Code First Migrations in Team Environments (Code First-Migrationen in Teamumgebungen)
Hinweis
In diesem Artikel wird davon ausgegangen, dass Ihnen bekannt ist, wie Sie Code First-Migrationen in Basisszenarien verwenden. Wenn nicht, sollten Sie Code First-Migrationen lesen, bevor Sie fortfahren.
Nehmen Sie sich einen Kaffee und lesen Sie den Artikel komplett durch.
In Teamumgebungen entstehen die meisten Probleme durch das Zusammenführen von Migrationen, wenn zwei Entwickler Migrationen in ihrer lokalen Codebasis erstellt haben. Während die Schritte zur Lösung dieser Probleme ziemlich einfach sind, benötigen Sie ein solides Verständnis der Funktionsweise von Migrationen. Bitte springen Sie nicht einfach zum Ende – nehmen Sie sich die Zeit, den ganzen Artikel zu lesen, um späteren Erfolg zu gewährleisten.
Einige allgemeine Richtlinien
Bevor wir uns mit der Zusammenführung von Migrationen befassen, die von mehreren Entwicklern erstellt wurden, möchten wir Ihnen einige allgemeine Richtlinien an die Hand geben, die Ihnen den Erfolg sichern sollen.
Jedes Teammitglied sollte über eine lokale Entwicklungsdatenbank verfügen.
Migrationen verwenden die Tabelle __MigrationsHistory, um zu speichern, welche Migrationen auf die Datenbank angewendet wurden. Wenn Sie mehrere Entwickler haben, die unterschiedliche Migrationen erstellen und dabei versuchen, dieselbe Datenbank anzusteuern (und somit eine gemeinsame __MigrationsHistory-Tabelle nutzen), werden die Migrationen sehr unübersichtlich werden.
Wenn Sie Teammitglieder haben, die keine Migrationen erstellen, ist es natürlich kein Problem, wenn sie sich eine zentrale Entwicklungsdatenbank teilen.
Vermeiden automatischer Migrationen
Die Quintessenz ist, dass automatische Migrationen in Teamumgebungen zunächst gut aussehen, aber in der Realität einfach nicht funktionieren. Wenn Sie wissen wollen, warum, lesen Sie weiter – wenn nicht, können Sie zum nächsten Abschnitt übergehen.
Automatische Migrationen ermöglichen es Ihnen, Ihr Datenbankschema zu aktualisieren, damit es dem aktuellen Modell entspricht, ohne dass Sie Codedateien erstellen müssen (codebasierte Migrationen). Automatische Migrationen würden in einer Teamumgebung sehr gut funktionieren, wenn Sie sie nur verwenden und nie codebasierte Migrationen erstellen würden. Das Problem ist, dass automatische Migrationen begrenzt sind und eine Reihe von Operationen nicht beherrschen – Umbenennung von Eigenschaften/Spalten, Verschieben von Daten in eine andere Tabelle usw. Für diese Szenarien müssen Sie codebasierte Migrationen erstellen (und den gerüsteten Code bearbeiten), die zwischen die Änderungen gemischt werden, die von den automatischen Migrationen verarbeitet werden. Dadurch ist es nahezu unmöglich, Änderungen zusammenzuführen, wenn zwei Entwickler Migrationen einchecken.
Grundlegendes zur Funktionsweise von Migrationen
Der Schlüssel zur erfolgreichen Verwendung von Migrationen in einer Teamumgebung ist ein grundlegendes Verständnis dafür, wie Migrationen Informationen zum Modell nachverfolgen und verwenden, um Modelländerungen zu erkennen.
Die erste Migration
Wenn Sie die erste Migration zu Ihrem Projekt hinzufügen, führen Sie etwa Add-Migration First in der Paket-Manager-Konsole aus. Die allgemeinen Schritte, die dieser Befehl ausführt, sind unten dargestellt.
Das aktuelle Modell wird aus dem Code (1) berechnet. Die erforderlichen Datenbankobjekte werden dann vom Modellvergleich (2) berechnet – da dies die erste Migration ist, verwendet der Modellvergleich einfach ein leeres Modell für den Vergleich. Die erforderlichen Änderungen werden an den Codegenerator übergeben, um den erforderlichen Migrationscode (3) zu erstellen, der dann Ihrer Visual Studio-Projektmappe (4) hinzugefügt wird.
Zusätzlich zum tatsächlichen Migrationscode, der in der Hauptcodedatei gespeichert ist, generiert Migrationen auch zusätzliche CodeBehind-Dateien. Bei diesen Dateien handelt es sich um Metadaten, die von Migrationen verwendet werden und von Ihnen nicht bearbeitet werden sollten. Eine dieser Dateien ist eine Ressourcendatei (RESX), die eine Momentaufnahme des Modells zum Zeitpunkt der Migration enthält. Im nächsten Schritt sehen Sie, wie sie verwendet wird.
An diesem Punkt würden Sie wahrscheinlich Update-Database ausführen, um Ihre Änderungen auf die Datenbank anzuwenden, und sich dann an die Implementierung anderer Bereiche Ihrer Anwendung machen.
Nachfolgende Migrationen
Später kehren Sie zurück und nehmen einige Änderungen an Ihrem Modell vor – in unserem Beispiel fügen wir eine Url-Eigenschaft zu Blog hinzu. Anschließend würden Sie einen Befehl wie Add-Migration AddUrl ausgeben, um ein Gerüst der Migration zu erstellen und die entsprechenden Datenbankänderungen anzuwenden. Die allgemeinen Schritte, die dieser Befehl ausführt, sind unten dargestellt.
Genau wie beim letzten Mal wird das aktuelle Modell aus Code (1) berechnet. Dieses Mal gibt es jedoch bereits Migrationen, sodass das vorherige Modell aus der neuesten Migration (2) abgerufen wird. Diese beiden Modelle werden miteinander verglichen, um die erforderlichen Änderungen in der Datenbank zu finden (3) und dann wird der Prozess wie zuvor abgeschlossen.
Dieser Vorgang wird für alle weiteren Migrationen verwendet, die Sie dem Projekt hinzufügen.
Warum sollten Sie sich die Mühe mit der Momentaufnahme des Modells machen?
Sie fragen sich vielleicht, warum EF eine Momentaufnahme des Modells verwendet. Warum nicht einfach die Datenbank betrachten? Wenn ja, lesen Sie weiter. Wenn nicht, können Sie diesen Abschnitt überspringen.
Es gibt eine Reihe von Gründen, warum EF die Momentaufnahme des Modells beibehält:
- Dadurch kann sich die Datenbank vom EF-Modell entfernen. Diese Änderungen können direkt in der Datenbank vorgenommen werden, oder Sie können den Gerüstcode in Ihren Migrationen ändern, um die Änderungen vorzunehmen. Hier sind einige praxisnahe Beispiele dafür:
- Sie möchten einer oder mehreren Ihrer Tabellen eine Spalte „Inserted“ und „Updated“ hinzufügen, aber Sie möchten diese Spalten nicht in das EF-Modell aufnehmen. Wenn Migrationen die Datenbank betrachteten, würde ständig versucht werden, diese Spalten jedes Mal abzulegen, wenn Sie ein Gerüst für eine Migration erstellt haben. Mithilfe der Momentaufnahme des Modells erkennt EF nur legitime Änderungen am Modell.
- Sie möchten den Textkörper einer gespeicherten Prozedur ändern, die für Aktualisierungen verwendet wird, um Protokollierung einzuschließen. Wenn Migrationen diese gespeicherte Prozedur von der Datenbank aus betrachten würden, würden sie ständig versuchen, sie auf die Definition zurückzusetzen, die EF erwartet. Durch die Verwendung der Momentaufnahme des Modells wird EF immer nur dann Code zur Änderung der gespeicherten Prozedur einbauen, wenn Sie die Form der Prozedur im EF-Modell ändern.
- Dieselben Prinzipien gelten für das Hinzufügen zusätzlicher Indizes, einschließlich zusätzlicher Tabellen in Ihrer Datenbank, Zuordnen von EF zu einer Datenbankansicht, die sich über einer Tabelle befindet, usw.
- Das EF-Modell enthält mehr als nur die Form der Datenbank. Mit dem gesamten Modell können Migrationen Informationen zu den Eigenschaften und Klassen in Ihrem Modell und deren Zuordnung zu den Spalten und Tabellen anzeigen. Dank dieser Informationen können Migrationen in dem Code, für den sie ein Gerüst erstellen, intelligenter sein. Wenn Sie beispielsweise den Namen der Spalte ändern, der eine Eigenschaft zugeordnet ist, können Migrationen die Umbenennung erkennen, indem sie feststellen, dass es sich um dieselbe Eigenschaft handelt – was nicht möglich ist, wenn Sie nur das Datenbankschema haben.
Ursachen von Problemen in Teamumgebungen
Der im vorigen Abschnitt beschriebene Workflow funktioniert hervorragend, wenn Sie als einzelner Entwickler an einer Anwendung arbeiten. Er funktioniert auch gut in einer Teamumgebung, wenn Sie die einzige Person sind, die Änderungen am Modell vornimmt. In diesem Szenario können Sie Modelländerungen vornehmen, Migrationen generieren und an die Quellcodeverwaltung übermitteln. Andere Entwickler können Ihre Änderungen synchronisieren und Update-Database ausführen, damit die Schemaänderungen angewendet werden.
Probleme treten auf, wenn mehrere Entwickler Änderungen am EF-Modell vornehmen und gleichzeitig an die Quellcodeverwaltung übermitteln. Was EF fehlt, ist eine erstklassige Möglichkeit, Ihre lokalen Migrationen mit Migrationen zusammenzuführen, die ein anderer Entwickler seit der letzten Synchronisierung an die Quellcodeverwaltung übermittelt hat.
Beispiel für einen Zusammenführungskonflikt
Sehen wir uns zunächst ein konkretes Beispiel für einen solchen Zusammenführungskonflikt an. Wir verwenden dafür das Beispiel, das wir zuvor betrachtet haben. Gehen wir davon aus, dass die Änderungen aus dem vorherigen Abschnitt vom ursprünglichen Entwickler eingecheckt wurden. Wir verfolgen zwei Entwickler, während sie Änderungen an der Codebasis vornehmen.
Wir verfolgen das EF-Modell und die Migrationen über eine Reihe von Änderungen. Als Ausgangspunkt haben sich beide Entwickler mit dem Repository der Versionskontrolle synchronisiert, wie in der folgenden Grafik zu sehen ist.
Entwickler #1 und Entwickler #2 nehmen nun einige Änderungen am EF-Modell in ihrer lokalen Codebasis vor. Entwickler #1 fügt Blog eine Rating-Eigenschaft hinzu – und erzeugt eine AddRating-Migration, um die Änderungen in der Datenbank zu übernehmen. Entwickler #2 fügt Blog eine Readers-Eigenschaft hinzu – und erzeugt die entsprechende AddReaders-Migration. Beide Entwickler führen Update-Database aus, um die Änderungen auf ihre lokalen Datenbanken anzuwenden, und fahren Sie dann mit der Entwicklung der Anwendung fort.
Hinweis
Den Migrationen ist ein Zeitstempel vorangestellt. Unsere Grafik zeigt also, dass die AddReaders-Migration von Entwickler #2 nach der AddRating-Migration von Entwickler #1 erfolgt. Ob Entwickler #1 oder #2 die Migration zuerst erstellt hat, macht keinen Unterschied für die Arbeit im Team oder für den Prozess der Zusammenführung, den wir uns im nächsten Abschnitt ansehen werden.
Es ist ein Glückstag für Entwickler #1, da er seine Änderungen zuerst übermitteln. Da seit der Synchronisierung des Repositorys niemand anderes eingecheckt hat, kann er seine Änderungen einfach einreichen, ohne eine Zusammenführung durchzuführen.
Jetzt ist es an der Zeit, dass Entwickler #2 seine Änderungen übermittelt. Er hat nicht so viel Glück. Da jemand anderes seit der Synchronisierung Änderungen übermittelt hat, muss er die Änderungen herunterziehen und zusammenführen. Das Quellcodeverwaltungssystem wird wahrscheinlich in der Lage sein, die Änderungen auf Code-Ebene automatisch zusammenzuführen, da sie sehr einfach sind. Der Status des lokalen Repository von Entwickler #2 nach der Synchronisierung ist in der folgenden Grafik dargestellt.
In dieser Phase kann Entwickler #2 Update-Database ausführen, das die neue AddRating-Migration (die noch nicht auf die Datenbank von Entwickler #2 angewendet wurde) erkennt und anwendet. Jetzt wird die Spalte Rating zur Tabelle Blogs hinzugefügt, und die Datenbank wird mit dem Modell synchronisiert.
Es gibt jedoch einige Probleme:
- Obwohl Update-Database die AddRating-Migration anwendet, wird auch eine Warnung ausgegeben: Die Datenbank kann nicht an das aktuelle Modell angepasst werden, da noch Änderungen ausstehen und die automatische Migration deaktiviert ist... Das Problem besteht darin, dass der Momentaufnahme des Modells, die bei der letzten Migration (AddReader) gespeichert wurde, die Eigenschaft Rating für Blog fehlt (da sie nicht Teil des Modells war, als die Migration erstellt wurde). Code First erkennt, dass das Modell in der letzten Migration nicht mit dem aktuellen Modell übereinstimmt, und löst die Warnung aus.
- Das Ausführen der Anwendung würde zu einer InvalidOperationException führen, die besagt: „Das Modell, das dem Kontext 'BloggingContext' zugrunde liegt, wurde seit der Erstellung der Datenbank geändert. Erwägen Sie die Verwendung von Code First-Migrationen, um die Datenbank zu aktualisieren...“ Auch hier besteht das Problem darin, dass die bei der letzten Migration gespeicherte Momentaufnahme des Modells nicht mit dem aktuellen Modell übereinstimmt.
- Schließlich würden wir erwarten, dass die Ausführung von Add-Migration jetzt eine leere Migration erzeugt (da es keine Änderungen gibt, die auf die Datenbank anzuwenden sind). Da Migrationen jedoch das aktuelle Modell mit dem Modell der letzten Migration vergleicht (in dem die Eigenschaft Rating fehlt), wird ein weiterer AddColumn-Aufruf erstellt, um die Spalte Rating hinzuzufügen. Natürlich würde diese Migration bei Update-Database fehlschlagen, da die Spalte Rating bereits existiert.
Beheben des Zusammenführungskonflikts
Die gute Nachricht ist, dass es nicht allzu schwierig ist, die Zusammenführung manuell durchzuführen – vorausgesetzt, Sie wissen, wie Migrationen funktionieren. Wenn Sie also das Vorherige übersprungen haben… Leider müssen Sie zuerst den Rest des Artikels lesen!
Es gibt zwei Möglichkeiten. Die einfachste ist, eine leere Migration zu erstellen, die das richtige aktuelle Modell als Momentaufnahme enthält. Die zweite Möglichkeit besteht darin, die Momentaufnahme bei der letzten Migration zu aktualisieren, um den richtigen Modell-Snapshot zu erhalten. Die zweite Option ist etwas schwieriger und kann nicht in jedem Szenario verwendet werden, aber sie ist auch sauberer, da sie nicht das Hinzufügen einer zusätzlichen Migration erfordert.
Option 1: Hinzufügen einer leeren „Zusammenführungsmigration“
Bei dieser Option erzeugen wir eine leere Migration, um sicherzustellen, dass in der letzten Migration die richtige Momentaufnahme des Modells gespeichert ist.
Diese Option kann unabhängig davon verwendet werden, wer die letzte Migration generiert hat. In unserem Beispiel kümmert sich Entwickler #2 um die Zusammenführung und hat zufällig die letzte Migration erstellt. Die gleichen Schritte können jedoch auch durchgeführt werden, wenn Entwickler #1 die letzte Migration erstellt hat. Die Schritte gelten auch für mehrere Migrationen – wir haben uns der Einfachheit halber auf zwei beschränkt.
Der folgende Prozess kann für diesen Ansatz verwendet werden, beginnend mit dem Zeitpunkt, an dem Sie feststellen, dass Sie Änderungen haben, die aus der Quellcodeverwaltung synchronisiert werden müssen.
- Stellen Sie sicher, dass alle ausstehenden Modelländerungen in Ihrer lokalen Codebasis in eine Migration geschrieben wurden. Dieser Schritt stellt sicher, dass Sie keine legitimen Änderungen verpassen, wenn es um die Erstellung der leeren Migration geht.
- Synchronisieren Sie mit Quellcodeverwaltung.
- Führen Sie Update-Database aus, um alle neuen Migrationen anzuwenden, die andere Entwickler eingecheckt haben. Hinweis: Wenn Sie keine Warnungen von dem Befehl „Update-Database“ erhalten, gab es keine neuen Migrationen von anderen Entwicklern, und Sie müssen keine weitere Zusammenführung durchführen.
- Führen Sie Add-Migration <pick_a_name> –IgnoreChanges aus (zum Beispiel Add-Migration Merge –IgnoreChanges). Dies erzeugt eine Migration mit allen Metadaten (einschließlich einer Momentaufnahme des aktuellen Modells), ignoriert aber alle Änderungen, die es beim Vergleich des aktuellen Modells mit der Momentaufnahme in den letzten Migrationen feststellt (d. h. Sie erhalten eine leere Up- und Down-Methode).
- Führen Sie Update-Database aus, um die neueste Migration mit den aktualisierten Metadaten erneut anzuwenden.
- Fahren Sie mit der Entwicklung fort, oder übermitteln Sie sie an die Quellcodeverwaltung (nach dem Ausführen der Komponententests natürlich).
Nachfolgend sehen Sie den Status der lokalen Codebasis von Entwickler #2, nachdem Sie diesen Ansatz verwendet haben.
Option 2: Aktualisieren der Momentaufnahme des Modells in der letzten Migration
Diese Option ist Option 1 sehr ähnlich, entfernt aber die zusätzliche leere Migration – denn seien wir ehrlich, wer will schon zusätzliche Codedateien in seiner Lösung.
Dieser Ansatz ist nur möglich, wenn die neueste Migration nur in Ihrer lokalen Codebasis vorhanden ist und noch nicht an die Quellcodeverwaltung übermittelt wurde (z. B. wenn die letzte Migration vom Benutzer generiert wurde, der die Zusammenführung ausführt). Die Bearbeitung der Metadaten von Migrationen, die andere Entwickler bereits auf ihre Entwicklungsdatenbank – oder schlimmer noch auf eine Produktionsdatenbank – angewendet haben, kann zu unerwarteten Nebeneffekten führen. Während des Prozesses wird ein Rollback der letzten Migration in unserer lokalen Datenbank ausgeführt und mit aktualisierten Metadaten erneut angewendet.
Während die letzte Migration nur in der lokalen Codebasis stattfinden muss, gibt es keine Einschränkungen hinsichtlich der Anzahl oder Reihenfolge der Migrationen, die ihr vorausgehen. Es kann mehrere Migrationen von verschiedenen Entwicklern geben und es gelten dieselben Schritte – wir haben uns der Einfachheit halber nur zwei angesehen.
Der folgende Prozess kann für diesen Ansatz verwendet werden, beginnend mit dem Zeitpunkt, an dem Sie feststellen, dass Sie Änderungen haben, die aus der Quellcodeverwaltung synchronisiert werden müssen.
- Stellen Sie sicher, dass alle ausstehenden Modelländerungen in Ihrer lokalen Codebasis in eine Migration geschrieben wurden. Dieser Schritt stellt sicher, dass Sie keine legitimen Änderungen verpassen, wenn es um die Erstellung der leeren Migration geht.
- Synchronisieren Sie mit der Quellcodeverwaltung.
- Führen Sie Update-Database aus, um alle neuen Migrationen anzuwenden, die andere Entwickler eingecheckt haben. Hinweis: Wenn Sie keine Warnungen von dem Befehl „Update-Database“ erhalten, gab es keine neuen Migrationen von anderen Entwicklern, und Sie müssen keine weitere Zusammenführung durchführen.
- Führen Sie Update-Database –TargetMigration <second_last_migration> aus (in unserem Beispiel wäre das Update-Database –TargetMigration AddRating). Dadurch wird die Datenbank in den Zustand der vorletzten Migration zurückversetzt, d. h. die letzte Migration wird aus der Datenbank „entfernt“. Hinweis: Dieser Schritt ist erforderlich, damit Sie die Metadaten der Migration sicher bearbeiten können, denn die diese werden auch in der __MigrationsHistoryTable der Datenbank gespeichert. Deshalb sollten Sie diese Option nur verwenden, wenn sich die letzte Migration nur in Ihrer lokalen Codebasis befindet. Wenn die letzte Migration auf andere Datenbanken angewendet wurde, müssen Sie diese ebenfalls zurücksetzen und die letzte Migration erneut anwenden, um die Metadaten zu aktualisieren.
- Führen Sie Add-Migration <full_name_including_timestamp_of_last_migration> aus (in unserem Beispiel in etwa Add-Migration 201311062215252_AddReaders). Hinweis: Sie müssen den Zeitstempel angeben, damit Migrationen wissen, dass Sie die vorhandene Migration bearbeiten möchten, anstatt eine neue zu erstellen. Dadurch werden die Metadaten für die letzte Migration so aktualisiert, dass sie mit dem aktuellen Modell übereinstimmen. Sie erhalten die folgende Warnung, wenn der Befehl abgeschlossen ist, aber das ist genau das, gewünscht ist. “Nur der Designercode für die Migration "201311062215252_AddReaders" wurde als Gerüst neu erstellt. Verwenden Sie den Parameter -Force, um das Gerüst für die gesamte Migration erneut zu erstellen.”
- Führen Sie Update-Database aus, um die neueste Migration mit den aktualisierten Metadaten erneut anzuwenden.
- Fahren Sie mit der Entwicklung fort, oder übermitteln Sie sie an die Quellcodeverwaltung (nach dem Ausführen der Komponententests natürlich).
Nachfolgend sehen Sie den Status der lokalen Codebasis von Entwickler #2, nachdem Sie diesen Ansatz verwendet haben.
Zusammenfassung
Es gibt einige Herausforderungen bei der Verwendung von Code First-Migrationen in einer Teamumgebung. Ein grundlegendes Verständnis der Funktionsweise von Migrationen und einige einfache Ansätze zur Lösung von Zusammenführungskonflikten erleichtern jedoch die Bewältigung dieser Herausforderungen.
Das grundlegende Problem sind falsche Metadaten, die in der letzten Migration gespeichert wurden. Dies führt dazu, dass Code First fälschlicherweise erkennt, dass das aktuelle Modell und das Datenbankschema nicht übereinstimmen, und bei der nächsten Migration falschen Code einbaut. Diese Situation kann überwunden werden, indem eine leere Migration mit dem richtigen Modell generiert wird oder die Metadaten in der neuesten Migration aktualisiert werden.