
Era uma vez um projeto no EF 6 com o MSSQL DBMS. E havia a necessidade de adicionar a capacidade de trabalhar com o PostgreSQL. Não esperávamos problemas aqui, porque há um grande número de artigos sobre esse tópico e, nos fóruns, é possível encontrar uma discussão sobre problemas semelhantes. No entanto, na realidade, nem tudo acabou sendo tão simples, e neste artigo falaremos sobre essa experiência, sobre os problemas que encontramos durante a integração do novo provedor e sobre a solução que escolhemos.
Introdutório
Temos um produto in a box, e ele tem uma estrutura já estabelecida. Inicialmente, foi configurado para funcionar com um DBMS - MSSQL. O projeto possui uma camada de acesso a dados com a implementação do EF 6 (abordagem Code First). Trabalhamos com migrações através do EF 6 Migrations. As migrações são criadas manualmente. A instalação inicial do banco de dados ocorre a partir do aplicativo do console com a inicialização do contexto na cadeia de conexão, passada 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; } }
Ao mesmo tempo, a infraestrutura EF e o domínio do domínio são descritos em outro projeto, conectado ao aplicativo do console como uma biblioteca. O construtor de contexto no projeto de infraestrutura se parece com isso:
public class MyDbContext : IdentityDbContext<User, Role, Key, UserLogin, UserRole, UserClaim>, IUnitOfWork { public MyDbContext(string connectionString) : base(connectionString) { Database.SetInitializer(new DbInitializer()); Database.Initialize(true); } }
Primeiro lançamento
A primeira coisa que fizemos foi conectar dois pacotes ao projeto via nuget: Npgsql e EntityFramework6.Npgsql.
Também registramos as configurações do Postgres no App.config do nosso aplicativo de console.
A seção entityFramework especificou a fábrica padrão do postgres como a fábrica de conexões:
<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>
Na seção DbProviderFactories, a fábrica do novo provedor foi registrada:
<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>
E imediatamente, eles tentaram inicializar o banco de dados especificando o endereço do servidor Postgres e as credenciais do administrador do servidor na cadeia de conexão. O resultado é a seguinte linha:
“Servidor = host local; DataBase = TestPostgresDB; Segurança Integrada = false; ID do usuário = postgres; senha = pa $$ w0rd "
Como esperado, graças ao modo manual de Migrações EF, a inicialização não passou e ocorreu um erro que não corresponde à imagem do banco de dados do modelo atual. Para contornar a criação da migração primária com o novo provedor e testar a inicialização do banco de dados no Postgres, ajustamos ligeiramente nossa configuração de infraestrutura.
Primeiro, ativamos as “migrações automáticas” - uma opção útil se um desenvolvedor fizer alterações nos modelos de domínio e na infraestrutura da EF na equipe:
public sealed class Configuration : DbMigrationsConfiguration<MyDbContext> { public Configuration() { AutomaticMigrationsEnabled = true; ContextKey = "Project.Infrastructure.MyDbContext"; } }
Em segundo lugar, especificamos um novo provedor no método InitializeDatabase substituído da classe herdada CreateDatabaseIfNotExists, onde as migrações são iniciadas aqui:
public class DbInitializer : CreateDatabaseIfNotExists<MyDbContext> { public override void InitializeDatabase(MyDbContext context) { DbMigrator dbMigrator = new DbMigrator(new Configuration {
Em seguida, lançamos nosso aplicativo de console novamente com a mesma cadeia de conexão que um argumento. Desta vez, a inicialização do contexto foi sem erros e nossos modelos de domínio se encaixam com segurança no novo banco de dados do Postgres. O rótulo "__MigrationHistory" apareceu no novo banco de dados, no qual havia um único registro da primeira migração criada automaticamente.
Resumindo: conseguimos conectar um novo provedor a um projeto existente sem problemas, mas ao mesmo tempo alteramos as configurações do mecanismo de migração.
Ativar o modo de migração manual
Como mencionado acima, quando o modo de migração automática está ativado, você priva sua equipe de desenvolvimento paralelo nas áreas de domínio e acesso a dados. Para nós, essa opção era inaceitável. Portanto, precisávamos configurar um modo manual de migrações no projeto.
Primeiro, retornamos o campo AutomaticMigrationsEnabled para false. Então foi necessário lidar com a criação de novas migrações. Entendemos que as migrações para diferentes DBMSs, pelo menos, deveriam ser armazenadas em diferentes pastas do projeto. Portanto, decidimos criar uma nova pasta para migrações do Postgres em um projeto de infraestrutura chamado PostgresMigrations (a pasta com migrações do MsSql, para maior clareza, renomeamos como MsSqlMigrations) e copiamos o arquivo de configuração da migração do MsSql. Ao mesmo tempo, não copiamos todas as migrações existentes do MsSql para o PostgresSql. Primeiro, porque todos eles contêm um instantâneo da configuração para o provedor MsSql e, portanto, não poderemos usá-los no novo DBMS. Em segundo lugar, o histórico de alterações não é importante para o novo DBMS e podemos seguir com o instantâneo mais recente do estado dos modelos de domínio.
Pensamos que tudo estava pronto para a formação da primeira migração para o Postgres. O banco de dados criado durante a inicialização do contexto com o modo de migração automática ativado foi excluído. E, guiados pelo fato de que, para a primeira migração, você precisa criar um banco de dados físico com base no estado atual dos modelos de domínio, pontuamos com satisfação o comando Update-Database no Console do Gerenciador de Pacotes, especificando apenas o parâmetro da cadeia de conexão. Como resultado, obtivemos um erro relacionado à conexão com o DBMS.
Depois de estudar o princípio de funcionamento do comando Update-Database, fizemos o seguinte:
- adicionou o seguinte código às definições de configuração de migração:
para MsSql:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"MsSqlMigrations"; }
para o Postgres:
public Configuration() { AutomaticMigrationsEnabled = false; ContextKey = "Project.Infrastructure.MyDbContext"; MigrationsDirectory = @"PostgresMigrations"; }
- indicou o parâmetro necessário do comando Update-Database passando o nome do provedor
- parâmetros adicionados que indicam o projeto que contém a descrição da infraestrutura ef e a pasta com a configuração de migração do novo provedor
Como resultado, obtivemos este comando:
Update-Database -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestPostgresDB; Segurança Integrada = false; ID do usuário = postgres; senha = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Após executar este comando, conseguimos executar o comando Add-Migration com parâmetros semelhantes, nomeando a primeira migração InitialCreate:
Adicionar-Migração-Nome "InitialCreate" -ProjectName "CrossTech.DSS.Infrastructure" -ConfigurationTypeName CrossTech.DSS.Infrastructure.PostgresMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestPostgresDB; Segurança Integrada = false; ID do usuário = postgres; senha = pa $$ w0rd "-ConnectionProviderName" Npgsql "
Um novo arquivo apareceu na pasta PostgresMigrations: 2017010120705068_InitialCreate.cs
Em seguida, excluímos o banco de dados criado após a execução do comando Update-Database e lançamos nosso aplicativo de console com a cadeia de conexão indicada acima como argumento. E, por isso, já conseguimos o banco de dados com base em uma migração criada manualmente.
Resumindo: conseguimos, com o mínimo de esforço, adicionar a primeira migração para o provedor Postgres e inicializar o contexto por meio do aplicativo do console, obtendo o novo banco de dados no qual ocorreram as alterações de nossa primeira migração manual.
Alternar entre provedores
Ainda tínhamos uma pergunta em aberto: como configurar a inicialização de contexto para que fosse possível acessar um DBMS específico em tempo de execução?
A tarefa era que, no estágio de inicialização do contexto, fosse possível selecionar um ou outro banco de dados de destino do provedor desejado. Como resultado de tentativas repetidas de configurar essa opção, criamos uma solução parecida com esta.
No aplicativo de console do projeto em app.config (e se você não usar app.config, então machine.config), adicionamos uma nova cadeia de conexão com o provedor e o nome da conexão e, no construtor de contexto, descartamos o nome da conexão em vez da cadeia de conexão. Ao mesmo tempo, conectamos a própria cadeia de conexão ao contexto por meio do singleton da instância DbConfiguration. Passamos a instância da classe herdada de DbConfiguration como um parâmetro.
A classe DbConfiguration herdada 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; } } }
E a própria inicialização do contexto agora se parece com isso:
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"); }
E quem seguiu com cuidado, ele provavelmente notou que precisávamos fazer mais uma alteração no código. Essa é a definição do banco de dados de destino durante a inicialização do banco de dados, que ocorre no método InitializeDatabase descrito anteriormente.
Adicionamos um switch simples para determinar a configuração de migração de um provedor específico:
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);
E o próprio construtor de contexto começou a ficar assim:
public MyDbContext(string connectionNameParam) : base(connectionString) { Database.SetInitializer(new DbInitializer(connectionName = connectionNameParam)); Database.Initialize(true); }
Em seguida, lançamos o aplicativo do console e especificamos o parâmetro do aplicativo MsSql como o provedor DBMS. Definimos os argumentos para o aplicativo da seguinte maneira:
"MsSqlDbConnection" "Servidor = localhost \ SQLEXPRESS; Banco de Dados = TestMsSqlDB; ID do usuário = sa; senha = pa $$ w0rd "" System.Data.SqlClient "
O banco de dados MsSql foi criado sem erros.
Em seguida, especificamos os argumentos do aplicativo:
"PostgresDbConnection" "Servidor = host local; DataBase = TestPostgresDB; Segurança Integrada = false; ID do usuário = postgres; senha = pa $$ w0rd "" Npgsql "
O banco de dados do Postgres também foi criado sem erros.
Portanto, mais um subtotal - para que o EF inicialize o contexto do banco de dados para um provedor específico, em tempo de execução, você precisa:
- "Indique" o mecanismo de migração para esse provedor
- configurar cadeias de conexão DBMS antes da inicialização do contexto
Trabalhamos com as migrações de dois DBMSs em uma equipe
Como vimos, a parte mais interessante começa após o surgimento de novas alterações no domínio. Você precisa gerar migrações para dois DBMSs, levando em consideração um provedor específico.
Portanto, para o MSSQL Server, você precisa executar comandos seqüenciais (para o Postgres, os comandos descritos acima, ao criar a primeira migração):
- atualizando o banco de dados de acordo com o último instantâneo
Update-Database -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestMsSqlDB; Segurança Integrada = false; ID do usuário = sa; senha = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
- adicionando nova migração
Add-Migration -Name "SomeMigrationName" -ProjectName "Project.Infrastructure" -ConfigurationTypeName Project.Infrastructure.MsSqlMigrations.Configuration -ConnectionString "Server = localhost; DataBase = TestMsSqlDB; Segurança Integrada = false; ID do usuário = sa; senha = pa $$ w0rd "-ConnectionProviderName" System.Data.SqlClient "
Quando os desenvolvedores fazem alterações no domínio em paralelo, temos vários conflitos ao mesclar essas alterações no sistema de controle de versão (por simplicidade, chamaremos git). Isso se deve ao fato de que as migrações para a EF ocorrem sequencialmente, uma após a outra. E se um desenvolvedor cria uma migração, outro desenvolvedor simplesmente não consegue adicionar a migração sequencialmente. Cada migração subsequente armazena informações sobre a anterior. Portanto, é necessário atualizar os chamados snapshots de modelo na migração para o último criado.
Ao mesmo tempo, resolver conflitos nas migrações da EF em uma equipe se resume a priorizar o significado das alterações de um desenvolvedor em particular. E cujas alterações são de prioridade mais alta, essas devem ser as primeiras a preenchê-las no git, e o restante dos desenvolvedores de acordo com a hierarquia acordada precisa fazer o seguinte:
- excluir migrações locais criadas
- puxe as alterações do repositório para você, onde outros colegas com alta prioridade já fizeram suas migrações
- criar migração local e fazer upload das alterações resultantes de volta ao git
No que diz respeito ao mecanismo de migração da EF, podemos julgar que a abordagem de desenvolvimento de equipe descrita é a única no momento. Não consideramos essa solução ideal, mas ela tem direito à vida. E a questão de encontrar uma alternativa ao mecanismo de Migrações da EF tornou-se urgente para nós.
Em conclusão
Trabalhar com vários DBMSs usando o EF6 em conjunto com o EF Migrations é real, mas nesta versão os funcionários da Microsoft não levaram em conta a possibilidade de trabalho paralelo da equipe usando sistemas de controle de versão.
Existem muitas soluções alternativas de EF Migrations no mercado (pagas e gratuitas): DbUp, RoundhousE, ThinkingHome.Migrator, FluentMigrator, etc. E, a julgar pelas críticas, eles são mais parecidos com desenvolvedores do que com a EF Migrations.
Felizmente, agora temos a oportunidade de fazer algum tipo de atualização em nosso projeto. E em um futuro próximo, mudaremos para o EF Core. Avaliamos os prós e os contras do mecanismo EF Core Migrations e concluímos que seria mais conveniente trabalhar com uma solução de terceiros, o Fluent Migrator.
Esperamos que você esteja interessado em nossa experiência. Pronto para aceitar comentários e responder a perguntas, Wellcome!