Propagação de dados
A propagação de dados é o processo de preencher um banco de dados com um conjunto inicial de dados.
Há várias maneiras de fazer isso no EF Core:
- Propagação de dados de opções de configuração (
UseSeeding
) - Lógica de inicialização personalizada
- Dados gerenciados pelo modelo (
HasData
) - Personalização manual de migração
Métodos UseSeeding
e UseAsyncSeeding
de opções de configuração
O EF 9 introduziu os métodos UseSeeding
e UseAsyncSeeding
, que fornecem uma maneira prática de preencher o banco de dados com dados iniciais. Esses métodos visam melhorar a experiência de uso da lógica de inicialização personalizada (explicada abaixo). Eles fornecem um local claro onde todo o código de propagação de dados pode ser colocado. Além disso, o código dentro dos métodos UseSeeding
e UseAsyncSeeding
é protegido pelo mecanismo de bloqueio de migração para evitar problemas de simultaneidade.
Os novos métodos de propagação são chamados como parte da operação dotnet ef database update
, Migrate
e do comando dotnet ef database update
, mesmo que não haja alterações no modelo e nenhuma migração tenha sido aplicada.
Dica
Usar UseSeeding
and UseAsyncSeeding
é a maneira recomendada de preencher o banco de dados com dados iniciais ao trabalhar com o EF Core.
Esses métodos podem ser configurados na etapa de configuração de opções. Este é um exemplo:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
.UseSeeding((context, _) =>
{
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
await context.SaveChangesAsync(cancellationToken);
}
});
Observação
UseSeeding
é chamado no método EnsureCreated
e UseAsyncSeeding
é chamado no método EnsureCreatedAsync
. Ao usar esse recurso, é recomendável implementar os métodos UseSeeding
e UseAsyncSeeding
usando uma lógica semelhante, mesmo que o código que usa o EF seja assíncrono. Atualmente, as ferramentas do EF Core dependem da versão síncrona do método e não vão preencher o banco de dados corretamente se o método UseSeeding
não for implementado.
Lógica de inicialização personalizada
Uma maneira simples e poderosa de executar a propagação de dados é usar DbContext.SaveChanges()
antes que a lógica do aplicativo principal inicie a execução. Recomenda-se o uso de UseSeeding
e UseAsyncSeeding
para esse fim, mas, às vezes, usar esses métodos não é uma boa solução. Um cenário de exemplo é quando a propagação requer o uso de dois contextos diferentes em uma transação. Abaixo está um exemplo de código que executa a inicialização personalizada diretamente no aplicativo:
using (var context = new DataSeedingContext())
{
context.Database.EnsureCreated();
var testBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Blogs.Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
}
Aviso
O código de propagação não deve fazer parte da execução normal do aplicativo, pois isso pode causar problemas de simultaneidade quando várias instâncias estão em execução e também exigiria que o aplicativo tivesse permissão para modificar o esquema de banco de dados.
Dependendo das restrições da implantação, o código de inicialização pode ser executado de maneiras diferentes:
- Executando o aplicativo de inicialização localmente
- Implantando o aplicativo de inicialização com o aplicativo principal, invocando a rotina de inicialização e desabilitando ou removendo o aplicativo de inicialização.
Isso geralmente pode ser automatizado usando perfis de publicação.
Dados gerenciados pelo modelo
Os dados também podem ser associados a um tipo de entidade como parte da configuração do modelo. Assim, as migrações do EF Core podem calcular automaticamente quais operações inserir, atualizar ou excluir precisam ser aplicadas ao atualizar o banco de dados para uma nova versão do modelo.
Aviso
As migrações consideram apenas as alterações de modelo ao determinar qual operação deve ser executada para colocar os dados gerenciados no estado desejado. Portanto, quaisquer alterações nos dados executados fora das migrações podem ser perdidas ou causar um erro.
Por exemplo, isso configurará dados gerenciados para um Country
em OnModelCreating
:
modelBuilder.Entity<Country>(b =>
{
b.Property(x => x.Name).IsRequired();
b.HasData(
new Country { CountryId = 1, Name = "USA" },
new Country { CountryId = 2, Name = "Canada" },
new Country { CountryId = 3, Name = "Mexico" });
});
Para adicionar entidades que têm uma relação, os valores de chave estrangeira precisam ser especificados:
modelBuilder.Entity<City>().HasData(
new City { Id = 1, Name = "Seattle", LocatedInId = 1 },
new City { Id = 2, Name = "Vancouver", LocatedInId = 2 },
new City { Id = 3, Name = "Mexico City", LocatedInId = 3 },
new City { Id = 4, Name = "Puebla", LocatedInId = 3 });
Ao gerenciar dados para navegações muitos para muitos, a entidade de junção precisa ser configurada explicitamente. Se o tipo de entidade tiver propriedades no estado de sombra (por exemplo, a entidade de junção LanguageCountry
abaixo), uma classe anônima poderá ser usada para fornecer os valores:
modelBuilder.Entity<Language>(b =>
{
b.HasData(
new Language { Id = 1, Name = "English" },
new Language { Id = 2, Name = "French" },
new Language { Id = 3, Name = "Spanish" });
b.HasMany(x => x.UsedIn)
.WithMany(x => x.OfficialLanguages)
.UsingEntity(
"LanguageCountry",
r => r.HasOne(typeof(Country)).WithMany().HasForeignKey("CountryId").HasPrincipalKey(nameof(Country.CountryId)),
l => l.HasOne(typeof(Language)).WithMany().HasForeignKey("LanguageId").HasPrincipalKey(nameof(Language.Id)),
je =>
{
je.HasKey("LanguageId", "CountryId");
je.HasData(
new { LanguageId = 1, CountryId = 2 },
new { LanguageId = 2, CountryId = 2 },
new { LanguageId = 3, CountryId = 3 });
});
});
Os tipos de entidade de propriedade podem ser configurados de maneira semelhante:
modelBuilder.Entity<Language>().OwnsOne(p => p.Details).HasData(
new { LanguageId = 1, Phonetic = false, Tonal = false, PhonemesCount = 44 },
new { LanguageId = 2, Phonetic = false, Tonal = false, PhonemesCount = 36 },
new { LanguageId = 3, Phonetic = true, Tonal = false, PhonemesCount = 24 });
Consulte o projeto de exemplo completo para obter mais contexto.
Depois que os dados forem adicionados ao modelo, asmigrações deverão ser usadas para aplicar as alterações.
Dica
Se você precisar aplicar migrações como parte de uma implantação automatizada, poderá criar um script SQL que pode ser visualizado antes da execução.
Como alternativa, você pode usar context.Database.EnsureCreated()
para criar um novo banco de dados contendo os dados gerenciados, por exemplo, para um banco de dados de teste ou ao usar o provedor na memória ou qualquer banco de dados não relacional. Se o banco de dados já existir, EnsureCreated()
não atualizará o esquema nem os dados gerenciados no banco de dados. Para bancos de dados relacionais, você não deve chamar EnsureCreated()
se planeja usar migrações.
Observação
Preencher o banco de dados usando o método HasData
costumava ser chamado de "propagação de dados". Essa nomenclatura define expectativas incorretas, pois o recurso tem várias limitações e é apropriado apenas para tipos específicos de dados. É por isso que decidimos renomeá-lo para "dados gerenciados pelo modelo". Os métodos UseSeeding
e UseAsyncSeeding
devem ser usados para propagação de dados de uso geral.
Limitações dos dados gerenciados pelo modelo
Esse tipo de dados é gerenciado por migrações e o script para atualizar os dados que já estão no banco de dados precisa ser gerado sem se conectar ao banco de dados. Isso impõe algumas restrições:
- O valor da chave primária precisa ser especificado mesmo que geralmente seja gerado pelo banco de dados. Ele será usado para detectar alterações de dados entre migrações.
- Os dados inseridos anteriormente serão removidos se a chave primária for alterada de alguma forma.
Portanto, esse recurso é mais útil para dados estáticos que não devem ser alterados fora das migrações e não dependem de mais nada no banco de dados, por exemplo, CEP.
Se o cenário incluir qualquer um dos itens a seguir, é recomendável usar os métodos UseSeeding
e UseAsyncSeeding
descritos na primeira seção:
- Dados temporários para teste
- Dados que dependem do estado do banco de dados
- Dados grandes (a propagação de dados é capturada em instantâneos de migração e dados grandes podem rapidamente levar a arquivos enormes e desempenho degradado).
- Dados que precisam de valores de chave a serem gerados pelo banco de dados, incluindo entidades que usam chaves alternativas como a identidade
- Dados que exigem transformação personalizada (que não é tratada por conversões de valor), como alguns hashs de senha
- Dados que exigem chamadas à API externa, como ASP.NET funções de Identidade Principal e criação de usuários
- Dados que não são fixos e determinísticos, como a propagação para
DateTime.Now
.
Personalização manual de migração
Quando uma migração é adicionada, as alterações nos dados especificados com HasData
são transformados em chamadas para InsertData()
, UpdateData()
e DeleteData()
. Uma maneira de contornar algumas das limitações de HasData
é adicionar manualmente essas chamadas ou operações personalizadas à migração.
migrationBuilder.InsertData(
table: "Countries",
columns: new[] { "CountryId", "Name" },
values: new object[,]
{
{ 1, "USA" },
{ 2, "Canada" },
{ 3, "Mexico" }
});
migrationBuilder.InsertData(
table: "Languages",
columns: new[] { "Id", "Name", "Details_PhonemesCount", "Details_Phonetic", "Details_Tonal" },
values: new object[,]
{
{ 1, "English", 44, false, false },
{ 2, "French", 36, false, false },
{ 3, "Spanish", 24, true, false }
});
migrationBuilder.InsertData(
table: "Cites",
columns: new[] { "Id", "LocatedInId", "Name" },
values: new object[,]
{
{ 1, 1, "Seattle" },
{ 2, 2, "Vancouver" },
{ 3, 3, "Mexico City" },
{ 4, 3, "Puebla" }
});
migrationBuilder.InsertData(
table: "LanguageCountry",
columns: new[] { "CountryId", "LanguageId" },
values: new object[,]
{
{ 2, 1 },
{ 2, 2 },
{ 3, 3 }
});