Pontos de dados

Migrações do EF6 Code First para vários modelos

Julie Lerman

Baixar o código de exemplo

Julie LermanO Entity Framework 6 introduziu suporte para migrações do Code First para lidar melhor com o armazenamento de dados para vários modelos em um único banco de dados. Mas o suporte é muito específico e pode não ser o que você imagina. Neste artigo, você saberá mais sobre esse recurso, o que ele faz e não faz e como usá-lo.

Modelos distintos, tabelas distintas, dados distintos: Mesmo banco de dados

As migrações do EF6 dá suporte à migração de vários modelos que são completamente independentes um do outro. Ambas as implementações desse recurso — usando uma chave para identificar um conjunto de migrações ou usando o esquema do banco de dados para históricos de migração de grupo com as tabelas para um único modelo — permitem que você armazene modelos separados e distintos no mesmo banco de dados.

Não para compartilhar dados entre modelos ou para bancos de dados multilocatários

É fácil interpretar incorretamente os benefícios desse recurso, portanto, quero deixar claro imediatamente o que não é suportado.

Esse novo suporte a vários modelo não foi projetado para replicar um único modelo e entre vários esquemas para ter um banco de dados multilocatário.

O outro padrão que muitos de nós esperamos quando virmos que esse recurso é a capacidade de compartilhar uma entidade comum (e seus dados) entre vários modelos e mapear a entidade para um único banco de dados. No entanto, esse é um tipo de problema muito diferente e não é um problema que pode ser facilmente resolvido com o Entity Framework. Eu costumava tentar, mas desisti. Já escrevi artigos anteriores sobre o compartilhamento de dados entre bancos de dados nesta coluna (consulte "Um padrão para compartilhamento de dados entre contextos vinculados ao design controlado por domínio, Parte 2" em bit.ly/1817XNT). Também apresentei uma sessão no TechEd Europe chamado "Particionamento de modelo do Entity Framework em contextos vinculados em design controlado por domínio", que foi registrado e está disponível em bit.ly/1AI6xPa.

Padrão um: ContextKey é a chave

Uma das novas ferramentas que o EF6 fornece para habilitar esse recurso é ContextKey. Isso é um novo campo na tabela MigrationHistory do banco de dados que controla cada migração. Ela é compartilhada com uma nova propriedade de mesmo nome na classe DbMigrations­Configuration<TContext>.

Por padrão, o ContextKey herdará o nome fortemente tipado do DbMigrationsConfiguration associado a esse contexto. Por exemplo, aqui está uma classe DbContext que funciona com tipos Doctor e Episode:

namespace ModelOne.Context
{
  public class ModelOneContext:DbContext
  {
    public DbSet<Doctor> Doctors { get; set; }
    public DbSet<Episode> Episodes { get; set; }
  }
}

Como sempre, o comportamento padrão do comando enable-migrations é criar uma classe DbMigrationsConfiguration que tem o nome [YourNamespace].Migrations.Configuration.

Ao aplicar uma determinada migração (isto é, quando eu chamar Update-­Database no Visual Studio Package Manager Console), o Entity Framework não só se aplicará a migração, também adicionará uma nova linha na tabela __MigrationHistory. Aqui está o SQL para essa ação:

INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model],
  [ProductVersion])
VALUES (N'201501131737236_InitialModelOne',
  N'ModelOne.Context.Migrations.Configuration',
  [hash of the model], N'6.1.2-31219')

Observe que o valor indo para o campo ContextKey é Model­One.Context.Migrations.Configuration, que é o nome fortemente tipado da minha classe DbMigrationsConfiguration<TContext>.

Você pode controlar o nome ContextKey especificando a propriedade Context­Key da classe DbMigrationsConfiguration no construtor de classe. Eu vou renomeá-lo para ModelOne:

public Configuration()
{
  AutomaticMigrationsEnabled = false;
  ContextKey = "ModelOne";
}

Agora executar migrações usará ModelOne para o campo ContextKey da tabela de migração. No entanto, se você já tiver executado as migrações com o padrão, isso não funcionará. O EF tentará reaplicar todas as migrações, incluindo aqueles que criou tabelas e outro objetos do banco de dados, fazendo com que o banco de dados gere erros devido aos objetos duplicados. Meu conselho é alterar esse valor antes de aplicar quaisquer migrações, caso contrário, você precisará atualizar manualmente os dados na tabela __MigrationsHistory.

Eu me certifiquei de que meu tipo DbContext aponte para uma cadeia de conexão chamada MultipleModelDb. Em vez de contar com a convenção do Code First para localizar uma cadeia de caracteres de conexão com o mesmo nome que o contexto, eu quero ter uma única cadeia de conexão que posso usar para qualquer modelo que tem como alvo este banco de dados. Eu fiz isso especificando que o construtor de contexto herda a sobrecarga de DbContext, que usa um nome da cadeia de caracteres de conexão. Aqui está o construtor para ModelOneContext:

public ModelOneContext()
       : base("MultipleModelDb") {
}

add-migration e update-database poderão encontrar a cadeia de conexão, portanto, eu tenho garantia da migração para o banco de dados correto.

Dois contextos, Dois ContextKeys

Agora que você viu como o ContextKey funciona, vamos adicionar outro modelo com seu próprio ContextKey. Coloquei esse modelo em um projeto separado. O padrão para fazer isso quando você tem vários modelos no mesmo projeto é um pouco diferente; vou demonstrar isso neste artigo. Este é meu novo modelo, ModelTwo:

namespace ModelTwo.Context
{
  public class ModelTwoContext:DbContext
  {
    public DbSet<BBCEmployee> BbcEmployees { get; set; }
    public DbSet<HiringHistory> HiringHistories { get; set; }
  }
}

O ModelTwoContext funciona com classes de domínio completamente diferentes. Aqui está sua classe DbConfiguration, onde eu especifiquei que ContextKey será chamado de ModelTwo:

internal sealed class Configuration : DbMigrationsConfiguration<ModelTwoContext>
  {
    public Configuration()
    {
      AutomaticMigrationsEnabled = false;
      ContextKey = "ModelTwo";
    }

Quando eu chamar update-database no projeto que contém ModelTwoContext, as novas tabelas são criadas no mesmo banco de dados e uma nova linha é adicionada à tabela __MigrationHistory. Desta vez, o valor ContextKey é ModelTwo, como você pode ver no trecho de código do SQL que foi executado pela migração:

INSERT [dbo].[__MigrationHistory]([MigrationId], [ContextKey], [Model], [ProductVersion])
VALUES (N'201501132001186_InitialModelTwo', N'ModelTwo', [hash of the model], N'6.1.2-31219')

Como posso desenvolver meu domínio, meu DbContext e meu banco de dados, as migrações de EF sempre verificarão a volta para o conjunto de migrações executadas relevantes na tabela __MigrationHistory usando o ContextKey apropriado. Dessa forma, será capaz de determinar quais alterações fazer no banco de dados, dada as alterações realizadas no modelo. Isso permite que o EF gerencie corretamente as migrações para vários modelos DbContext que são armazenados no banco de dados. Mas lembre-se de que só pode funcionar porque não há nenhuma sobreposição em relação às tabelas do banco de dados para as quais os dois modelos são mapeados.

Padrão Dois: Esquemas do banco de dados separam modelos e migrações

O outro padrão que você pode usar para permitir que as migrações funcionem com vários modelos em um único banco de dados é separar as migrações e as tabelas relevantes com esquemas do banco de dados. Isso é tão semelhante a simplesmente direcionar bancos de dados separados conforme você os recebe, sem alguns dos recursos de sobrecarga (por exemplo, manutenção e despesas), você pode ter vários bancos de dados.

O EF6 torna muito mais fácil definir um esquema de banco de dados para um único modelo, configurando-o com um novo mapeamento DbModelBuilder.HasSchema. Isso substituirá o nome do esquema padrão, que, para o SQL Server, é dbo.

Lembre-se de que, mesmo se você não especificar a chave de contexto, um nome padrão será usado. Não faz sentido em remover as propriedades de chave de contexto definidas para demonstrar como a propriedade HasSchema afeta as migrações.

Eu vou definir o esquema para cada uma das minhas duas classes de contexto no método OnModelCreating. Aqui está o código relevante para ModelTwoContext, que especifiquei como tendo um esquema chamado ModelTwo:

protected override void OnModelCreating(DbModelBuilder modelBuilder) {
    modelBuilder.HasDefaultSchema("ModelTwo");
  }

O outro contexto receberá o nome do esquema ModelOne.

O resultado é que todos os objetos do banco de dados nos quais o modelo­TwoContext mapeia estarão no esquema ModelTwo. Além disso, o EF também colocará a tabela __MigrationHistory deste modelo no esquema ModelTwo.

Para demonstrar isso de forma limpa, estou apontando para um banco de dados diferente que daqueles nos exemplos anteriores e estou aplicando todas as migrações. Tenha em mente que a configuração do método HasDefaultSchema é uma alteração de mapeamento e exige que você adicione uma nova migração para aplicar essa alteração ao banco de dados. A Figura 1 mostra as tabelas de migração e dados em seus esquemas separados.

Migrações e tabelas agrupadas em esquemas de banco de dados
Figura 1 Migrações e tabelas agrupadas em esquemas de banco de dados

Além disso, sempre que você interagir com as migrações para qualquer contexto, como elas estão relacionadas a seus esquemas individuais, o EF não terá um problema em mantê-las separadamente. Como lembrete, preste atenção ao padrão crítico aqui, ou seja, não há nenhuma sobreposição com tabelas mapeadas para os dois modelos.

Vários modelos em um único projeto

Os dois exemplos que você viu até agora — usando ContextKey ou o esquema do banco de dados para separar as migrações de modelo — foram definidas com cada modelo encapsulado em seu próprio projeto. Essa é a maneira que eu prefiro arquitetar minhas soluções. Mas também é possível e, em muitos casos, completamente razoável para ter seus modelos no mesmo projeto. Se você usar ContextKey ou o esquema do banco de dados para manter as migrações classificadas, é possível obter isso com a adição de apenas alguns parâmetros extras para os comandos do NuGet.

Para obter uma separação clara entre esses exemplos, criarei uma nova solução com as mesmas classes. Vou manter as classes de domínio em projetos separados, mas ambos os modelos no mesmo projeto, conforme mostrado na Figura 2.

Colocar várias classes DbContext em um único projeto
Figura 2 Colocar várias classes DbContext em um único projeto

Como você sabe, por padrão, enable-­migrações criará uma pasta chamada Migrations para um DbContext descoberto na sua solução. Se você tiver vários DbContexts como eu, enable-migrations não apenas selecionará aleatoriamente um DbContext para a criação de migrações; em vez disso, ele retornará uma mensagem muito útil solicitando que você use o parâmetro ContextTypeName para indicar qual DbContext usar. A mensagem é tão boa que você pode simplesmente copiar e colar da mensagem para executar os comandos necessários. Aqui está a mensagem retornada para meu projeto:

PM> enable-migrations
More than one context type was found in the assembly 'ModelOne.Context'.
To enable migrations for 'ModelOne.Context.ModelOneContext', use
 Enable-Migrations -ContextTypeName ModelOne.Context.ModelOneContext.
To enable migrations for 'ModelTwo.Context.ModelTwoContext', use
 Enable-Migrations -ContextTypeName ModelTwo.Context.ModelTwoContext.

Além do parâmetro –ContextTypeName, adicionarei o parâmetro MigrationsDirctory para nomear explicitamente a pasta para tornar mais fácil o gerenciamento dos ativos do projeto:

Enable-Migrations
-ContextTypeName ModelOne.Context.ModelOneContext
-MigrationsDirectory ModelOneMigrations

A Figura 3 mostra as novas pastas com suas classes Configuration que foram criadas para cada migração.

O resultado da especificação do DbContext e o nome do diretório ao habilitar migrações
Figura 3 O resultado da especificação do DbContext e o nome do diretório ao habilitar migrações

Executar Enable-Migrations também adiciona o código para as classes DbConfiguration, que as torna ciente do nome do diretório. Esta é a classe de configuração para ModelOneContext como um exemplo (o arquivo DbConfiguraton para ModelTwoContext definirá seu nome de diretório para ModelTwoMigrations, conforme designado):

internal sealed class Configuration : DbMigrationsConfiguration<ModelOne.Context.ModelOneContext>
  {
    public Configuration()
    {
      AutomaticMigrationsEnabled = false;
      MigrationsDirectory = @"ModelOneMigrations";
    }

Como agora tenho duas classes chamadas Configuration, serei forçado a qualificá-las totalmente sempre que quiser usá-las. Portanto, irei renomear primeiro para ModelOneDbConfig (conforme mostrado no código a seguir) e depois para ModelTwoDbConfig:

internal sealed class ModelOneDbConfig : DbMigrationsConfiguration<ModelOneContext>
  {
    public ModelOneDbConfig()
      {
        AutomaticMigrationsEnabled = false;
        MigrationsDirectory = @"ModelOneMigrations";
      }
  }

Você também pode especificar um ContextKey se deseja substituir o padrão, mas deixarei isso para depois. Lembre-se de que eu especifico o método de mapeamento HasDefaultSchema nas minhas classes DbContext, portanto, as tabelas do histórico de migração e outros objetos do banco de dados serão colocados em seus próprios esquemas.

Agora é hora de adicionar migrações para ambos os modelos e aplicá-las em meu banco de dados. Novamente, eu preciso direcionar o EF para qual modelo e migração usar. Apontando para o arquivo de configuração de migração, o EF saberá qual modelo usar e em qual diretório armazenar os arquivos de migração.

Aqui está o meu comando para adicionar uma migração para ContextOne (lembre-se de que alterei o nome da classe de configuração para que não precise usar o nome totalmente qualificado para ConfigurationTypeName):

add-migration Initial
  -ConfigurationTypeName ModelOneDbConfig

O arquivo de migração resultante é criado no diretório ModelOne­Migrations. Depois de fazer o mesmo para ModelTwo, também tenho um arquivo de migração no diretório ModelTwoMigrations.

Agora é hora de aplicar essas migrações. Será preciso especificar o ConfigurationTypeName novamente para que o EF saiba qual migração usar. Aqui está o comando para ModelOne:

update-database
  -ConfigurationTypeName ModelOneDbConfig

Eu irei executá-lo e, em seguida, o comando relevante para ModelTwo:

update-database
  -ConfigurationTypeName ModelTwoDbConfig

Depois de executar esses comandos, meu banco de dados tem a mesma aparência que tinha na Figura 1.

Como posso modificar meus modelos e adicionar e aplicar as migrações, preciso apenas lembrar de especificar a classe de configuração correta como um parâmetro em cada um dos comandos.

Ajuste correto com a modelagem de design controlado por domínio

Em uma coluna de Pontos de Dados de duas partes recente chamada "Um padrão para compartilhar dados entre contextos vinculados de design controlado por domínio", escrevi sobre o compartilhamento de dados entre domínios que são mantidos para separar bancos de dados. A Parte Um está em bit.ly/1wolxz2 e a Parte Dois está em bit.ly/1817XNT. Vários desenvolvedores mencionaram que manter um banco de dados local separado pode ser um fardo e pagar por bancos de dados separados que estão hospedados na nuvem pode ser caro. As técnicas que você aprendeu neste artigo para hospedar as tabelas e os dados para vários modelos em um único banco de dados podem ajudá-lo a simular a separação do banco de dados completo. Esse novo suporte em migrações EF6 oferece uma boa solução para os desenvolvedores.


Julie Lerman é MVP da Microsoft, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrar ela apresentando sobre o acesso a dados e outros tópicos do .NET em grupos de usuário e conferências pelo mundo todo. Ela bloga em thedatafarm.com/blog e é a autora do “Programming Entity Framework” (2010), bem como de Code First edition (2011) e DbContext edition (2012), todos de O’Reilly Media. Siga Julie no Twitter em twitter.com/julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Rowan Miller