
Il était une fois un projet sur EF 6 avec le SGBD MSSQL. Et il fallait ajouter la possibilité de travailler avec PostgreSQL. Nous ne nous attendions pas à des problèmes ici, car il existe un grand nombre d'articles sur ce sujet, et sur les forums, vous pouvez trouver une discussion sur des problèmes similaires. Cependant, en réalité, tout n'a pas été aussi simple, et dans cet article, nous parlerons de cette expérience, des problèmes que nous avons rencontrés lors de l'intégration du nouveau fournisseur et de la solution que nous avons choisie.
Introduction
Nous avons un produit en boîte, et il a une structure déjà établie. Initialement, il a été configuré pour fonctionner avec un SGBD - MSSQL. Le projet dispose d'une couche d'accès aux données avec implémentation EF 6 (approche Code First). Nous travaillons avec les migrations via EF 6 Migrations. Les migrations sont créées manuellement. L'installation initiale de la base de données se produit à partir de l'application console avec l'initialisation du contexte sur la chaîne de connexion, passée en argument:
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; } }
Dans le même temps, l'infrastructure EF et le domaine sont décrits dans un autre projet, qui est connecté à l'application console en tant que bibliothèque. Le constructeur de contexte dans le projet d'infrastructure ressemble à ceci:
public class MyDbContext : IdentityDbContext<User, Role, Key, UserLogin, UserRole, UserClaim>, IUnitOfWork { public MyDbContext(string connectionString) : base(connectionString) { Database.SetInitializer(new DbInitializer()); Database.Initialize(true); } }
Premier lancement
La première chose que nous avons faite a été de connecter deux packages au projet via nuget: Npgsql et EntityFramework6.Npgsql.
Nous avons également enregistré les paramètres de Postgres dans l'App.config de notre application console.
La section entityFramework a spécifié la fabrique de postgres par défaut comme fabrique de connexions:
<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>
Dans la section DbProviderFactories, l'usine du nouveau fournisseur a été enregistrée:
<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>
Et tout de suite, ils ont essayé d'initialiser la base de données en spécifiant l'adresse du serveur Postgres et les informations d'identification de l'administrateur du serveur dans la chaîne de connexion. Le résultat est la ligne suivante:
«Server = localhost; DataBase = TestPostgresDB; Sécurité intégrée = faux; ID utilisateur = postgres; mot de passe = pa $$ w0rd ”
Comme prévu, grâce au mode EF Migrations manuel, l'initialisation n'a pas réussi et une erreur s'est produite ne correspondant pas à l'image de la base de données du modèle actuel. Afin de contourner la création de la migration principale avec le nouveau fournisseur et tester l'initialisation de la base de données sur Postgres, nous avons légèrement ajusté la configuration de notre infrastructure.
Premièrement, nous avons activé les «migrations automatiques» - une option utile si un développeur modifie les modèles de domaine et l'infrastructure EF de l'équipe:
public sealed class Configuration : DbMigrationsConfiguration<MyDbContext> { public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Project.Infrastructure.MyDbContext"; } }
Deuxièmement, nous avons spécifié un nouveau fournisseur dans la méthode redéfinie InitializeDatabase de la classe héritée CreateDatabaseIfNotExists, où nous commençons les migrations:
public class DbInitializer : CreateDatabaseIfNotExists<MyDbContext> { public override void InitializeDatabase(MyDbContext context) { DbMigrator dbMigrator = new DbMigrator(new Configuration {
Ensuite, nous avons relancé notre application console avec la même chaîne de connexion comme argument. Cette fois, l'initialisation du contexte s'est déroulée sans erreur et nos modèles de domaine s'intègrent en toute sécurité dans la nouvelle base de données Postgres. L'étiquette «__MigrationHistory» est apparue dans la nouvelle base de données, dans laquelle il y avait un seul enregistrement de la première migration créée automatiquement.
Pour résumer: nous avons pu connecter un nouveau fournisseur à un projet existant sans aucun problème, mais en même temps, nous avons modifié les paramètres du mécanisme de migration.
Activer le mode de migration manuelle
Comme mentionné ci-dessus, lorsque le mode de migration automatique est activé, vous privez votre équipe de développement parallèle dans les domaines d'accès au domaine et aux données. Pour nous, cette option était inacceptable. Par conséquent, nous devions mettre en place un mode manuel de migrations dans le projet.
Tout d'abord, nous avons renvoyé le champ AutomaticMigrationsEnabled à false. Ensuite, il a fallu gérer la création de nouvelles migrations. Nous avons compris que les migrations pour différents SGBD, au moins, devaient être stockées dans différents dossiers de projet. Par conséquent, nous avons décidé de créer un nouveau dossier pour les migrations Postgres dans un projet d'infrastructure appelé PostgresMigrations (le dossier avec les migrations MsSql, pour plus de clarté, nous l'avons renommé MsSqlMigrations) et y avons copié le fichier de configuration de la migration MsSql. Dans le même temps, nous n'avons pas copié toutes les migrations MsSql existantes vers PostgresSql. Premièrement, car ils contiennent tous un instantané de la configuration du fournisseur MsSql et, par conséquent, nous ne pourrons pas les utiliser sur le nouveau SGBD. Deuxièmement, l'historique des modifications n'est pas important pour le nouveau SGBD, et nous pouvons nous en tirer avec le dernier instantané de l'état des modèles de domaine.
Nous pensions que tout était prêt pour la formation de la première migration vers Postgres. La base de données créée lors de l'initialisation du contexte avec le mode de migration automatique activé a été supprimée. Et, guidés par le fait que pour la première migration, vous devez créer une base de données physique basée sur l'état actuel des modèles de domaine, nous avons heureusement noté la commande Update-Database dans la console du gestionnaire de packages, en spécifiant uniquement le paramètre de chaîne de connexion. En conséquence, nous avons obtenu une erreur liée à la connexion au SGBD.
Après avoir étudié en plus le principe de fonctionnement de la commande Update-Database, nous avons fait ce qui suit:
- a ajouté le code suivant aux paramètres de configuration de la migration:
pour MsSql:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"MsSqlMigrations"; }
pour Postgres:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"PostgresMigrations"; }
- a indiqué le paramètre nécessaire de la commande Update-Database en passant le nom du fournisseur
- paramètres ajoutés qui indiquent le projet contenant la description de l'infrastructure ef et le dossier avec la configuration de migration du nouveau fournisseur
En conséquence, nous avons obtenu cette commande:
Update-Database -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestPostgresDB; Sécurité intégrée = faux; ID utilisateur = postgres; mot de passe = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Après avoir exécuté cette commande, nous avons pu exécuter la commande Add-Migration avec des paramètres similaires, en nommant la première migration InitialCreate:
Add-Migration -Name "InitialCreate" -ProjectName "CrossTech.DSS.Infrastructure" -ConfigurationTypeName CrossTech.DSS.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestPostgresDB; Sécurité intégrée = faux; ID utilisateur = postgres; mot de passe = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Un nouveau fichier est apparu dans le dossier PostgresMigrations: 2017010120705068_InitialCreate.cs
Ensuite, nous avons supprimé la base de données créée après l'exécution de la commande Update-Database et lancé notre application console avec la chaîne de connexion indiquée ci-dessus comme argument. Et donc nous avons déjà obtenu la base de données sur la base d'une migration créée manuellement.
Pour résumer: nous avons pu, avec un effort minimal, ajouter la première migration pour le fournisseur Postgres et initialiser le contexte via l'application console, obtenant une nouvelle base de données, dans laquelle les modifications de notre première migration manuelle sont entrées.
Basculer entre les fournisseurs
Nous avions encore une question ouverte: comment configurer l'initialisation du contexte afin qu'il soit possible d'accéder à un SGBD spécifique lors de l'exécution?
La tâche était qu'au stade d'initialisation du contexte, il était possible de sélectionner l'une ou l'autre base de données cible du fournisseur souhaité. À la suite de tentatives répétées de configurer ce commutateur, nous avons trouvé une solution qui ressemble à ceci.
Dans l'application console du projet dans app.config (et si vous n'utilisez pas app.config, puis machine.config), nous ajoutons une nouvelle chaîne de connexion avec le fournisseur et le nom de la connexion, et dans le constructeur de contexte, nous «déposons» le nom de connexion au lieu de la chaîne de connexion. Dans le même temps, nous connectons la chaîne de connexion elle-même au contexte via le singleton de l'instance DbConfiguration. Nous passons l'instance de la classe héritée de DbConfiguration en tant que paramètre.
La classe DbConfiguration héritée résultante:
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; } } }
Et l'initialisation du contexte lui-même ressemble maintenant à ceci:
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"); }
Et qui a suivi attentivement, il a probablement remarqué que nous devions faire un autre changement dans le code. Il s'agit de la définition de la base de données cible lors de l'initialisation de la base de données, qui se produit dans la méthode InitializeDatabase décrite précédemment.
Nous avons ajouté un simple commutateur pour déterminer la configuration de migration d'un fournisseur particulier:
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);
Et le constructeur de contexte lui-même a commencé à ressembler à ceci:
public MyDbContext(string connectionNameParam) : base(connectionString) { Database.SetInitializer(new DbInitializer(connectionName = connectionNameParam)); Database.Initialize(true); }
Ensuite, nous avons lancé l'application console et spécifié le paramètre d'application MsSql en tant que fournisseur de SGBD. Nous définissons les arguments de l'application comme suit:
"MsSqlDbConnection" "Server = localhost \ SQLEXPRESS; Base de données = TestMsSqlDB; ID utilisateur = sa; mot de passe = pa $$ w0rd "" System.Data.SqlClient "
La base de données MsSql a été créée sans erreur.
Ensuite, nous avons spécifié les arguments de l'application:
"PostgresDbConnection" "Server = localhost; DataBase = TestPostgresDB; Sécurité intégrée = faux; ID utilisateur = postgres; mot de passe = pa $$ w0rd "" Npgsql "
La base de données Postgres a également été créée sans erreur.
Donc, un sous-total de plus - pour qu'EF initialise le contexte de base de données pour un fournisseur spécifique, vous avez besoin en runtime:
- «Indiquez» le mécanisme de migration vers ce fournisseur
- configurer les chaînes de connexion au SGBD avant l'initialisation du contexte
Nous travaillons avec les migrations de deux SGBD en équipe
Comme nous l'avons vu, la partie la plus intéressante commence après l'apparition de nouveaux changements dans le domaine. Vous devez générer des migrations pour deux SGBD en tenant compte d'un fournisseur spécifique.
Ainsi, pour MSSQL Server, vous devez exécuter des commandes séquentielles (pour Postgres, les commandes décrites ci-dessus, lors de la création de la première migration):
- mise à jour de la base de données selon le dernier instantané
Update-Database -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestMsSqlDB; Sécurité intégrée = faux; ID utilisateur = sa; mot de passe = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
- ajout d'une nouvelle migration
Add-Migration -Name "SomeMigrationName" -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestMsSqlDB; Sécurité intégrée = faux; ID utilisateur = sa; mot de passe = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
Lorsque les développeurs apportent des modifications au domaine en parallèle, nous obtenons plusieurs conflits lors de la fusion de ces modifications dans le système de contrôle de version (pour plus de simplicité, nous appellerons git). Cela est dû au fait que les migrations vers EF se font séquentiellement les unes après les autres. Et si un développeur crée une migration, un autre développeur ne réussira tout simplement pas à ajouter la migration de manière séquentielle. Chaque migration suivante stocke des informations sur la précédente. Ainsi, il est nécessaire de mettre à jour les soi-disant instantanés de modèle dans la migration vers le dernier créé.
Dans le même temps, la résolution des conflits sur les migrations EF dans une équipe revient à prioriser l'importance des changements d'un développeur particulier. Et dont les changements sont plus prioritaires, ceux-ci devraient être les premiers à les remplir en git, et les autres développeurs selon la hiérarchie convenue doivent faire ce qui suit:
- supprimer les migrations locales créées
- tirez les modifications du référentiel vers vous-même, où d'autres collègues de haute priorité ont déjà déversé leurs migrations
- créer une migration locale et télécharger les modifications résultantes dans git
Pour autant que nous connaissions le mécanisme de migration EF, nous pouvons juger que l'approche de développement d'équipe décrite est la seule pour le moment. Nous ne considérons pas cette solution comme idéale, mais elle a droit à la vie. Et la question de trouver une alternative au mécanisme EF Migrations est devenue urgente pour nous.
En conclusion
Travailler avec plusieurs SGBD utilisant EF6 en conjonction avec EF Migrations est réel, mais dans cette version, les gars de Microsoft n'ont pas pris en compte la possibilité d'un travail parallèle de l'équipe utilisant des systèmes de contrôle de version.
Il existe de nombreuses solutions alternatives EF Migrations sur le marché (payantes et gratuites): DbUp, RoundhousE, ThinkingHome.Migrator, FluentMigrator, etc. Et à en juger par les critiques, ils ressemblent plus à des développeurs qu'à EF Migrations.
Heureusement, nous avons maintenant la possibilité de faire une sorte de mise à niveau dans notre projet. Et dans un avenir proche, nous passerons à EF Core. Nous avons pesé les avantages et les inconvénients du mécanisme EF Core Migrations et sommes arrivés à la conclusion qu'il serait plus pratique pour nous de travailler avec une solution tierce, à savoir Fluent Migrator.
Nous espérons que vous avez été intéressé par notre expérience. Prêt à accepter les commentaires et à répondre aux questions, bienvenue!