
Había una vez un proyecto en EF 6 con el DBMS MSSQL. Y era necesario agregar la capacidad de trabajar con PostgreSQL. No esperábamos problemas aquí, porque hay una gran cantidad de artículos sobre este tema, y en los foros puede encontrar una discusión sobre problemas similares. Sin embargo, en realidad, no todo resultó ser tan simple, y en este artículo hablaremos sobre esta experiencia, sobre los problemas que encontramos durante la integración del nuevo proveedor y sobre la solución que elegimos.
Introductorio
Tenemos un producto en caja y tiene una estructura ya establecida. Inicialmente, se configuró para funcionar con un DBMS: MSSQL. El proyecto tiene una capa de acceso a datos con implementación EF 6 (enfoque Code First). Trabajamos con migraciones a través de EF 6 Migraciones. Las migraciones se crean manualmente. La instalación inicial de la base de datos ocurre desde la aplicación de consola con la inicialización del contexto en la cadena de conexión, pasada como argumento:
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; } }
Al mismo tiempo, la infraestructura de EF y el dominio de dominio se describen en otro proyecto, que está conectado a la aplicación de consola como una biblioteca. El constructor de contexto en el proyecto de infraestructura se ve así:
public class MyDbContext : IdentityDbContext<User, Role, Key, UserLogin, UserRole, UserClaim>, IUnitOfWork { public MyDbContext(string connectionString) : base(connectionString) { Database.SetInitializer(new DbInitializer()); Database.Initialize(true); } }
Primer lanzamiento
Lo primero que hicimos fue conectar dos paquetes al proyecto a través de nuget: Npgsql y EntityFramework6.Npgsql.
También registramos la configuración de Postgres en la configuración de la aplicación de nuestra aplicación de consola.
La sección entityFramework especificó la fábrica de postgres predeterminada como la fábrica de conexiones:
<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>
En la sección DbProviderFactories, se registró la fábrica del nuevo proveedor:
<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>
Y de inmediato, intentaron inicializar la base de datos especificando la dirección del servidor Postgres y las credenciales del administrador del servidor en la cadena de conexión. El resultado es la siguiente línea:
"Servidor = localhost; Base de datos = TestPostgresDB; Seguridad integrada = falso; Id de usuario = postgres; contraseña = pa $$ w0rd ”
Como se esperaba, gracias al modo manual de Migraciones EF, la inicialización no pasó y se produjo un error que no coincide con la imagen de la base de datos del modelo actual. Para evitar la creación de la migración primaria con el nuevo proveedor y probar la inicialización de la base de datos en Postgres, ajustamos ligeramente nuestra configuración de infraestructura.
En primer lugar, activamos las "migraciones automáticas", una opción útil si un desarrollador realiza cambios en los modelos de dominio y la infraestructura de EF en el equipo:
public sealed class Configuration : DbMigrationsConfiguration<MyDbContext> { public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Project.Infrastructure.MyDbContext"; } }
En segundo lugar, especificamos un nuevo proveedor en el método redefinido InitializeDatabase de la clase heredada CreateDatabaseIfNotExists, donde comenzamos las migraciones:
public class DbInitializer : CreateDatabaseIfNotExists<MyDbContext> { public override void InitializeDatabase(MyDbContext context) { DbMigrator dbMigrator = new DbMigrator(new Configuration {
Luego, lanzamos nuestra aplicación de consola nuevamente con la misma cadena de conexión como argumento. Esta vez, la inicialización del contexto se realizó sin errores, y nuestros modelos de dominio se ajustan de manera segura a la nueva base de datos de Postgres. La etiqueta "__MigrationHistory" apareció en la nueva base de datos, en la que había un registro único de la primera migración creada automáticamente.
En resumen: pudimos conectar un nuevo proveedor a un proyecto existente sin ningún problema, pero al mismo tiempo cambiamos la configuración del mecanismo de migración.
Activar el modo de migración manual
Como se mencionó anteriormente, cuando el modo de migración automática está activado, priva a su equipo de desarrollo paralelo en las áreas de dominio y acceso a datos. Para nosotros, esta opción era inaceptable. Por lo tanto, necesitábamos configurar un modo manual de migraciones en el proyecto.
Primero, devolvimos el campo AutomaticMigrationsEnabled a falso. Entonces fue necesario lidiar con la creación de nuevas migraciones. Entendemos que las migraciones para diferentes DBMS, al menos, deben almacenarse en diferentes carpetas de proyectos. Por lo tanto, decidimos crear una nueva carpeta para las migraciones de Postgres en un proyecto de infraestructura llamado PostgresMigrations (la carpeta con migraciones de MsSql, para mayor claridad, le cambiamos el nombre a MsSqlMigrations), y copiamos el archivo de configuración de migración de MsSql. Al mismo tiempo, no copiamos todas las migraciones MsSql existentes a PostgresSql. En primer lugar, porque todos contienen una instantánea de la configuración del proveedor MsSql y, en consecuencia, no podremos usarlos en el nuevo DBMS. En segundo lugar, el historial de cambios no es importante para el nuevo DBMS, y podemos pasar con la última instantánea del estado de los modelos de dominio.
Pensamos que todo estaba listo para la formación de la primera migración a Postgres. Se eliminó la base de datos creada durante la inicialización del contexto con el modo de migración automática activado. Y, guiados por el hecho de que para la primera migración necesita crear una base de datos física basada en el estado actual de los modelos de dominio, calificamos felizmente el comando Actualizar-Base de datos en la Consola del Administrador de paquetes, especificando solo el parámetro de cadena de conexión. Como resultado, obtuvimos un error relacionado con la conexión al DBMS.
Habiendo estudiado adicionalmente el principio de funcionamiento del comando Update-Database, hicimos lo siguiente:
- agregó el siguiente código a la configuración de la migración:
para MsSql:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"MsSqlMigrations"; }
para Postgres:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"PostgresMigrations"; }
- indicó el parámetro necesario del comando Actualizar-Base de datos que pasa el nombre del proveedor
- parámetros agregados que indican el proyecto que contiene la descripción de la infraestructura ef, y la carpeta con la configuración de migración del nuevo proveedor
Como resultado, obtuvimos este comando:
Update-Database -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; Base de datos = TestPostgresDB; Seguridad integrada = falso; Id de usuario = postgres; contraseña = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Después de ejecutar este comando, pudimos ejecutar el comando Add-Migration con parámetros similares, nombrando la primera migración InitialCreate:
Add-Migration -Name "InitialCreate" -ProjectName "CrossTech.DSS.Infrastructure" -ConfigurationTypeName CrossTech.DSS.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; Base de datos = TestPostgresDB; Seguridad integrada = falso; Id de usuario = postgres; contraseña = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Ha aparecido un nuevo archivo en la carpeta PostgresMigrations: 2017010120705068_InitialCreate.cs
Luego eliminamos la base de datos creada después de ejecutar el comando Update-Database y lanzamos nuestra aplicación de consola con la cadena de conexión indicada anteriormente como argumento. Y así obtuvimos la base de datos ya sobre la base de una migración creada manualmente.
Para resumir: pudimos, con un mínimo esfuerzo, agregar la primera migración para el proveedor de Postgres e inicializar el contexto a través de la aplicación de consola, obteniendo una nueva base de datos, en la que se produjeron los cambios de nuestra primera migración manual.
Cambiar entre proveedores
Todavía teníamos una pregunta abierta: ¿cómo configurar la inicialización de contexto para que fuera posible acceder a un DBMS específico en tiempo de ejecución?
La tarea consistía en que en la etapa de inicialización del contexto era posible seleccionar una u otra base de datos de destino del proveedor deseado. Como resultado de repetidos intentos de configurar este interruptor, se nos ocurrió una solución que se parece a esto.
En la aplicación de consola del proyecto en app.config (y si no usa app.config, luego machine.config), agregamos una nueva cadena de conexión con el proveedor y el nombre de la conexión, y en el constructor de contexto "soltamos" el nombre de la conexión en lugar de la cadena de conexión. Al mismo tiempo, conectamos la cadena de conexión al contexto a través del singleton de la instancia de DbConfiguration. Pasamos la instancia de la clase heredada de DbConfiguration como parámetro.
La clase DbConfiguration heredada resultante:
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; } } }
Y la inicialización del contexto en sí ahora se ve así:
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"); }
Y quien siguió con cuidado, probablemente notó que teníamos que hacer un cambio más en el código. Esta es la definición de la base de datos de destino durante la inicialización de la base de datos, que ocurre en el método InitializeDatabase descrito anteriormente.
Agregamos un interruptor simple para determinar la configuración de migración de un proveedor en particular:
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);
Y el propio constructor de contexto comenzó a verse así:
public MyDbContext(string connectionNameParam) : base(connectionString) { Database.SetInitializer(new DbInitializer(connectionName = connectionNameParam)); Database.Initialize(true); }
Luego, lanzamos la aplicación de consola y especificamos el parámetro de la aplicación MsSql como proveedor de DBMS. Establecemos los argumentos para la aplicación de la siguiente manera:
Servidor "MsSqlDbConnection" "= localhost \ SQLEXPRESS; Base de datos = TestMsSqlDB; Id de usuario = sa; contraseña = pa $$ w0rd "" System.Data.SqlClient "
La base de datos MsSql se creó sin errores.
Luego especificamos los argumentos de la aplicación:
"PostgresDbConnection" "Servidor = localhost; Base de datos = TestPostgresDB; Seguridad integrada = falso; Id de usuario = postgres; contraseña = pa $$ w0rd "" Npgsql "
La base de datos Postgres también se creó sin errores.
Entonces, un subtotal más: para que EF inicialice el contexto de la base de datos para un proveedor específico, en tiempo de ejecución necesita:
- "Indique" el mecanismo de migración a este proveedor
- configurar cadenas de conexión DBMS antes de la inicialización de contexto
Trabajamos con las migraciones de dos DBMS en un equipo.
Como vimos, la parte más interesante comienza después de la aparición de nuevos cambios en el dominio. Debe generar migraciones para dos DBMS teniendo en cuenta un proveedor específico.
Entonces, para el servidor MSSQL, debe ejecutar comandos secuenciales (para Postgres, los comandos descritos anteriormente, al crear la primera migración):
- actualizar la base de datos de acuerdo con la última instantánea
Actualizar-Base de datos -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; Base de datos = TestMsSqlDB; Seguridad integrada = falso; Id de usuario = sa; contraseña = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
- agregando nueva migración
Add-Migration -Name "SomeMigrationName" -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; Base de datos = TestMsSqlDB; Seguridad integrada = falso; Id de usuario = sa; contraseña = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
Cuando los desarrolladores realizan cambios en el dominio en paralelo, obtenemos múltiples conflictos al fusionar estos cambios en el sistema de control de versiones (por simplicidad llamaremos git). Esto se debe al hecho de que las migraciones a EF van secuencialmente una tras otra. Y si un desarrollador crea una migración, entonces otro desarrollador simplemente no logrará agregar la migración secuencialmente. Cada migración posterior almacena información sobre la anterior. Por lo tanto, es necesario actualizar las llamadas instantáneas del modelo en la migración a la última creada.
Al mismo tiempo, la resolución de conflictos en las migraciones de EF en un equipo se reduce a priorizar la importancia de los cambios de un desarrollador en particular. Y cuyos cambios tienen mayor prioridad, esos deberían ser los primeros en completarlos en git, y el resto de los desarrolladores de acuerdo con la jerarquía acordada deben hacer lo siguiente:
- eliminar migraciones locales creadas
- lleve los cambios del repositorio a usted mismo, donde otros colegas con alta prioridad ya han vertido sus migraciones
- crear migración local y subir los cambios resultantes de nuevo a git
En la medida en que estemos familiarizados con el mecanismo de migración de EF, podemos juzgar que el enfoque de desarrollo de equipo descrito es el único en este momento. No consideramos que esta solución sea ideal, pero tiene derecho a la vida. Y la cuestión de encontrar una alternativa al mecanismo de Migraciones EF se ha vuelto urgente para nosotros.
En conclusión
Trabajar con varios DBMS que usan EF6 junto con EF Migraciones es real, pero en esta versión los chicos de Microsoft no tomaron en cuenta la posibilidad de un trabajo paralelo del equipo usando sistemas de control de versiones.
Hay muchas soluciones alternativas de EF Migraciones en el mercado (tanto de pago como gratuitas): DbUp, RoundhousE, ThinkingHome.Migrator, FluentMigrator, etc. Y a juzgar por las revisiones, son más como desarrolladores que EF Migraciones.
Afortunadamente, ahora tenemos la oportunidad de realizar algún tipo de actualización en nuestro proyecto. Y en el futuro cercano cambiaremos a EF Core. Analizamos los pros y los contras del mecanismo de EF Core Migrations y llegamos a la conclusión de que sería más conveniente para nosotros trabajar con una solución de terceros, a saber, Fluent Migrator.
Esperamos que te haya interesado nuestra experiencia. Listo para aceptar comentarios y responder preguntas, ¡Bienvenido!