
Es war einmal ein Projekt auf EF 6 mit dem MSSQL DBMS. Außerdem musste die Möglichkeit hinzugefügt werden, mit PostgreSQL zu arbeiten. Wir haben hier keine Probleme erwartet, da es eine große Anzahl von Artikeln zu diesem Thema gibt und Sie in den Foren eine Diskussion über ähnliche Probleme finden. In Wirklichkeit stellte sich jedoch nicht alles als so einfach heraus, und in diesem Artikel werden wir über diese Erfahrung, über die Probleme, die bei der Integration des neuen Anbieters aufgetreten sind, und über die von uns gewählte Lösung sprechen.
Einführung
Wir haben ein Boxprodukt und es hat eine bereits etablierte Struktur. Ursprünglich war es für die Arbeit mit einem DBMS - MSSQL konfiguriert. Das Projekt verfügt über eine Datenzugriffsschicht mit EF 6-Implementierung (Code First-Ansatz). Wir arbeiten mit Migrationen über EF 6 Migrations. Migrationen werden manuell erstellt. Die Erstinstallation der Datenbank erfolgt über die Konsolenanwendung mit der Initialisierung des Kontexts in der Verbindungszeichenfolge, die als Argument übergeben wird:
static void Main(string[] args) { if (args.Length == 0) { throw new Exception("No arguments in command line"); } var connectionString = args[0]; Console.WriteLine($"Initializing dbcontext via {connectionString}"); try { using (var context = MyDbContext(connectionString)) { Console.WriteLine("Database created"); } } catch (Exception e) { Console.WriteLine(e.Message); throw; } }
Gleichzeitig werden die EF-Infrastruktur und die Domänendomäne in einem anderen Projekt beschrieben, das als Bibliothek mit der Konsolenanwendung verbunden ist. Der Kontextkonstruktor im Infrastrukturprojekt sieht folgendermaßen aus:
public class MyDbContext : IdentityDbContext<User, Role, Key, UserLogin, UserRole, UserClaim>, IUnitOfWork { public MyDbContext(string connectionString) : base(connectionString) { Database.SetInitializer(new DbInitializer()); Database.Initialize(true); } }
Erster Start
Als erstes haben wir zwei Pakete über Nuget mit dem Projekt verbunden: Npgsql und EntityFramework6.Npgsql.
Wir haben auch die Einstellungen für Postgres in der App.config unserer Konsolenanwendung registriert.
Im Abschnitt entityFramework wurde die Standard-Postgres-Factory als Verbindungsfactory angegeben:
<entityFramework> <defaultConnectionFactory type="Npgsql.NpgsqlConnectionFactory, EntityFramework6.Npgsql" /> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> <provider invariantName="Npgsql" type="Npgsql.NpgsqlServices, EntityFramework6.Npgsql" /> </providers> </entityFramework>
Im Bereich DbProviderFactories wurde die Fabrik des neuen Anbieters registriert:
<system.data> <DbProviderFactories> <add name="Npgsql Data Provider" invariant="Npgsql" support="FF" description=".Net Framework Data Provider for Postgresql" type="Npgsql.NpgsqlFactory, Npgsql" /> </DbProviderFactories> </system.data>
Und sofort versuchten sie, die Datenbank zu initialisieren, indem sie die Adresse des Postgres-Servers und die Anmeldeinformationen des Serveradministrators in der Verbindungszeichenfolge angaben. Das Ergebnis ist die folgende Zeile:
“Server = localhost; Datenbank = TestPostgresDB; Integrierte Sicherheit = falsch; Benutzer-ID = postgres; Passwort = pa $$ w0rd ”
Wie erwartet wurde die Initialisierung dank des manuellen EF-Migrationsmodus nicht bestanden, und es trat ein Fehler auf, der nicht mit dem Datenbankabbild des aktuellen Modells übereinstimmte. Um die Erstellung der primären Migration mit dem neuen Anbieter zu umgehen und die Datenbankinitialisierung auf Postgres zu testen, haben wir unsere Infrastrukturkonfiguration leicht angepasst.
Erstens haben wir "Auto-Migrationen" aktiviert - eine nützliche Option, wenn ein Entwickler Änderungen an den Domänenmodellen und der EF-Infrastruktur im Team vornimmt:
public sealed class Configuration : DbMigrationsConfiguration<MyDbContext> { public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Project.Infrastructure.MyDbContext"; } }
Zweitens haben wir in der neu definierten Methode InitializeDatabase der geerbten Klasse CreateDatabaseIfNotExists einen neuen Anbieter angegeben, mit dem wir die Migrationen starten:
public class DbInitializer : CreateDatabaseIfNotExists<MyDbContext> { public override void InitializeDatabase(MyDbContext context) { DbMigrator dbMigrator = new DbMigrator(new Configuration {
Als Nächstes haben wir unsere Konsolenanwendung erneut mit derselben Verbindungszeichenfolge wie ein Argument gestartet. Diesmal verlief die Initialisierung des Kontexts fehlerfrei, und unsere Domänenmodelle passen sicher in die neue Postgres-Datenbank. Das Label "__MigrationHistory" wurde in der neuen Datenbank angezeigt, in der ein einziger Datensatz der ersten automatisch erstellten Migration vorhanden war.
Zusammenfassend: Wir konnten problemlos einen neuen Anbieter mit einem vorhandenen Projekt verbinden, gleichzeitig aber die Einstellungen des Migrationsmechanismus ändern.
Aktivieren Sie den manuellen Migrationsmodus
Wie oben erwähnt, entziehen Sie Ihrem Team bei aktiviertem automatischen Migrationsmodus die parallele Entwicklung in den Bereichen Domäne und Datenzugriff. Für uns war diese Option nicht akzeptabel. Daher mussten wir im Projekt einen manuellen Migrationsmodus einrichten.
Zuerst haben wir das Feld AutomaticMigrationsEnabled auf false zurückgesetzt. Dann war es notwendig, sich mit der Schaffung neuer Migrationen zu befassen. Wir haben verstanden, dass Migrationen für verschiedene DBMS zumindest in verschiedenen Projektordnern gespeichert werden sollten. Aus diesem Grund haben wir beschlossen, einen neuen Ordner für Postgres-Migrationen in einem Infrastrukturprojekt namens PostgresMigrations zu erstellen (der Ordner mit MsSql-Migrationen wurde aus Gründen der Übersichtlichkeit in MsSqlMigrations umbenannt) und die Konfigurationsdatei für die MsSql-Migration in diesen Ordner kopiert. Gleichzeitig haben wir nicht alle vorhandenen MsSql-Migrationen nach PostgresSql kopiert. Erstens, da sie alle einen Snapshot der Konfiguration für den MsSql-Anbieter enthalten und wir sie dementsprechend nicht auf dem neuen DBMS verwenden können. Zweitens ist der Verlauf der Änderungen für das neue DBMS nicht wichtig, und wir können mit dem neuesten Schnappschuss des Status von Domänenmodellen auskommen.
Wir dachten, dass alles bereit war für die Bildung der ersten Migration nach Postgres. Die Datenbank, die während der Initialisierung des Kontexts mit aktiviertem automatischen Migrationsmodus erstellt wurde, wurde gelöscht. Ausgehend von der Tatsache, dass Sie für die erste Migration eine physische Datenbank erstellen müssen, die auf dem aktuellen Status der Domänenmodelle basiert, haben wir den Befehl Update-Database in der Package Manager-Konsole mit der Angabe nur des Verbindungszeichenfolgenparameters bewertet. Infolgedessen ist beim Herstellen einer Verbindung zum DBMS ein Fehler aufgetreten.
Nachdem wir zusätzlich das Funktionsprinzip des Befehls Update-Database untersucht haben, haben wir Folgendes getan:
- Der Migrationskonfigurationseinstellungen wurde der folgende Code hinzugefügt:
für MsSql:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"MsSqlMigrations"; }
für Postgres:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"PostgresMigrations"; }
- gab den erforderlichen Parameter des Befehls Update-Database an, wobei der Name des Anbieters übergeben wurde
- Es wurden Parameter hinzugefügt, die das Projekt mit der Beschreibung der ef-Infrastruktur und den Ordner mit der Migrationskonfiguration des neuen Anbieters angeben
Als Ergebnis haben wir diesen Befehl erhalten:
Update-Database -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; Datenbank = TestPostgresDB; Integrierte Sicherheit = falsch; Benutzer-ID = postgres; password = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Nachdem wir diesen Befehl ausgeführt hatten, konnten wir den Befehl Add-Migration mit ähnlichen Parametern ausführen und die erste Migration benennen. InitialCreate:
Add-Migration -Name "InitialCreate" -ProjectName "CrossTech.DSS.Infrastructure" -ConfigurationTypeName CrossTech.DSS.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; Datenbank = TestPostgresDB; Integrierte Sicherheit = falsch; Benutzer-ID = postgres; password = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Eine neue Datei wurde im Ordner PostgresMigrations angezeigt: 2017010120705068_InitialCreate.cs
Anschließend haben wir die nach dem Ausführen des Befehls Update-Database erstellte Datenbank gelöscht und unsere Konsolenanwendung mit der oben als Argument angegebenen Verbindungszeichenfolge gestartet. Und so haben wir die Datenbank bereits auf Basis einer manuell erstellten Migration erhalten.
Zusammenfassend lässt sich sagen, dass wir mit minimalem Aufwand die erste Migration für den Postgres-Anbieter hinzufügen und den Kontext über die Konsolenanwendung initialisieren konnten, um eine neue Datenbank zu erhalten, in die die Änderungen aus unserer ersten manuellen Migration übernommen wurden.
Zwischen Anbietern wechseln
Wir hatten noch eine offene Frage: Wie konfiguriere ich die Kontextinitialisierung so, dass zur Laufzeit auf ein bestimmtes DBMS zugegriffen werden kann?
Die Aufgabe bestand darin, dass in der Initialisierungsphase des Kontexts die eine oder andere Zieldatenbank des gewünschten Anbieters ausgewählt werden konnte. Als Ergebnis wiederholter Versuche, diesen Switch zu konfigurieren, haben wir eine Lösung gefunden, die so aussieht.
In der Konsolenanwendung des Projekts in app.config (und wenn Sie app.config nicht verwenden, dann machine.config) fügen wir eine neue Verbindungszeichenfolge mit dem Anbieter und dem Namen der Verbindung hinzu, und im Kontextkonstruktor "löschen" wir den Verbindungsnamen anstelle der Verbindungszeichenfolge. Gleichzeitig verbinden wir die Verbindungszeichenfolge selbst über den Singleton der DbConfiguration-Instanz mit dem Kontext. Wir übergeben die Instanz der geerbten Klasse von DbConfiguration als Parameter.
Die resultierende geerbte DbConfiguration-Klasse:
public class DbConfig : DbConfiguration { public DbConfig(string connectionName, string connectionString, string provideName) { ConfigurationManager.ConnectionStrings.Add(new ConnectionStringSettings(connectionName, connectionString, provideName)); switch (connectionName) { case "PostgresDbConnection": this.SetDefaultConnectionFactory(new NpgsqlConnectionFactory()); this.SetProviderServices(provideName, NpgsqlServices.Instance); this.SetProviderFactory(provideName, NpgsqlFactory.Instance); break; case "MsSqlDbConnection": this.SetDefaultConnectionFactory(new SqlConnectionFactory()); this.SetProviderServices(provideName, SqlProviderServices.Instance); this.SetProviderFactory(provideName, SqlClientFactory.Instance); this.SetDefaultConnectionFactory(new SqlConnectionFactory()); break; } } }
Und die Kontextinitialisierung selbst sieht jetzt so aus:
var connectionName = args[0]; var connectionString = args[1]; var provideName = args[2]; DbConfiguration.SetConfiguration(new DbConfig(connectionName, connectionString, provideName)); using (var context = MyDbContext(connectionName)) { Console.WriteLine("Database created"); }
Und wer genau folgte, bemerkte wahrscheinlich, dass wir den Code noch einmal ändern mussten. Dies ist die Definition der Zieldatenbank während der Datenbankinitialisierung, die in der zuvor beschriebenen InitializeDatabase-Methode erfolgt.
Wir haben einen einfachen Schalter hinzugefügt, um die Migrationskonfiguration eines bestimmten Anbieters zu bestimmen:
public class DbInitializer : CreateDatabaseIfNotExists<MyDbContext> { private string _connectionName; public DbInitializer(string connectionName) { _connectionName = connectionName; } public override void InitializeDatabase(MyDbContext context) { DbMigrationsConfiguration<MyDbContext> config; switch (_connectionName) { case "PostgresDbConnection": config = new PostgresMigrations.Configuration(); break; case "MsSqlDbConnection": config = new MsSqlMigrations.Configuration(); break; default: config = null; break; } if (config == null) return; config.TargetDatabase = new DbConnectionInfo(_connectionName); DbMigrator dbMigrator = new DbMigrator(config);
Und der Kontextkonstruktor selbst sah folgendermaßen aus:
public MyDbContext(string connectionNameParam) : base(connectionString) { Database.SetInitializer(new DbInitializer(connectionName = connectionNameParam)); Database.Initialize(true); }
Als Nächstes haben wir die Konsolenanwendung gestartet und den MsSql-Anwendungsparameter als DBMS-Anbieter angegeben. Wir setzen die Argumente für die Anwendung wie folgt:
"MsSqlDbConnection" "Server = localhost \ SQLEXPRESS; Datenbank = TestMsSqlDB; Benutzer-ID = sa; password = pa $$ w0rd "" System.Data.SqlClient "
Die MsSql-Datenbank wurde fehlerfrei erstellt.
Dann haben wir die Anwendungsargumente angegeben:
"PostgresDbConnection" "Server = localhost; Datenbank = TestPostgresDB; Integrierte Sicherheit = falsch; Benutzer-ID = postgres; Passwort = pa $$ w0rd "" Npgsql "
Die Postgres-Datenbank wurde ebenfalls fehlerfrei erstellt.
Also noch eine Zwischensumme - damit EF den Datenbankkontext für einen bestimmten Anbieter zur Laufzeit initialisieren kann, benötigen Sie:
- Geben Sie den Migrationsmechanismus für diesen Anbieter an
- Konfigurieren Sie DBMS-Verbindungszeichenfolgen vor der Kontextinitialisierung
Wir arbeiten mit den Migrationen von zwei DBMS in einem Team
Wie wir gesehen haben, beginnt der interessanteste Teil nach dem Auftreten neuer Änderungen in der Domäne. Sie müssen Migrationen für zwei DBMS unter Berücksichtigung eines bestimmten Anbieters generieren.
Für MSSQL Server müssen Sie also sequentielle Befehle ausführen (für Postgres die oben beschriebenen Befehle beim Erstellen der ersten Migration):
- Aktualisieren der Datenbank gemäß dem letzten Snapshot
Update-Database -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; Datenbank = TestMsSqlDB; Integrierte Sicherheit = falsch; Benutzer-ID = sa; password = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
- Hinzufügen einer neuen Migration
Add-Migration -Name "SomeMigrationName" -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; Datenbank = TestMsSqlDB; Integrierte Sicherheit = falsch; Benutzer-ID = sa; password = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
Wenn Entwickler parallel Änderungen an der Domäne vornehmen, treten beim Zusammenführen dieser Änderungen im Versionskontrollsystem mehrere Konflikte auf (der Einfachheit halber werden wir git nennen). Dies liegt an der Tatsache, dass Migrationen zu EF nacheinander erfolgen. Und wenn ein Entwickler eine Migration erstellt, gelingt es einem anderen Entwickler einfach nicht, die Migration nacheinander hinzuzufügen. Bei jeder nachfolgenden Migration werden Informationen zur vorherigen gespeichert. Daher ist es notwendig, die sogenannten Modell-Snapshots bei der Migration auf die zuletzt erstellte zu aktualisieren.
Gleichzeitig kommt es bei der Lösung von Konflikten bei EF-Migrationen in einem Team darauf an, die Bedeutung von Änderungen eines bestimmten Entwicklers zu priorisieren. Und deren Änderungen eine höhere Priorität haben, sollten diese als erste in git ausfüllen, und der Rest der Entwickler gemäß der vereinbarten Hierarchie muss Folgendes tun:
- Löschen Sie erstellte lokale Migrationen
- Ziehen Sie Änderungen aus dem Repository in sich selbst, wo andere Kollegen mit hoher Priorität bereits ihre Migrationen durchgeführt haben
- Erstellen Sie eine lokale Migration und laden Sie die resultierenden Änderungen zurück zu git
Soweit wir mit dem EF-Migrationsmechanismus vertraut sind, können wir beurteilen, dass der beschriebene Teamentwicklungsansatz derzeit der einzige ist. Wir halten diese Lösung nicht für ideal, aber sie hat das Recht auf Leben. Und die Frage, eine Alternative zum EF-Migrationsmechanismus zu finden, ist für uns dringend geworden.
Abschließend
Die Arbeit mit mehreren DBMS mit EF6 in Verbindung mit EF Migrations ist real, aber in dieser Version haben die Mitarbeiter von Microsoft die Möglichkeit einer parallelen Arbeit des Teams mit Versionskontrollsystemen nicht berücksichtigt.
Es gibt viele alternative EF Migrations-Lösungen auf dem Markt (kostenpflichtig und kostenlos): DbUp, RoundhousE, ThinkingHome.Migrator, FluentMigrator usw. Und nach den Bewertungen sind sie eher Entwickler als EF Migrations.
Glücklicherweise haben wir jetzt die Möglichkeit, ein Upgrade in unserem Projekt vorzunehmen. Und in naher Zukunft werden wir zu EF Core wechseln. Wir haben die Vor- und Nachteile des EF Core Migrations-Mechanismus abgewogen und sind zu dem Schluss gekommen, dass es für uns bequemer wäre, mit einer Drittanbieterlösung zu arbeiten, nämlich Fluent Migrator.
Wir hoffen, Sie haben sich für unsere Erfahrung interessiert. Bereit, Kommentare anzunehmen und Fragen zu beantworten. Willkommen!