Partilhar via


Semeadura 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:

Opções de configuração UseSeeding e métodos UseAsyncSeeding

O EF 9 introduziu métodos UseSeeding e UseAsyncSeeding, que fornecem uma maneira conveniente de semear 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). Isto fornece um local claro onde todo o código de inicializaçã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 semeadura são chamados como parte da operação EnsureCreated, Migrate e o comando dotnet ef database update, mesmo que não haja alterações no modelo e não tenha sido feita nenhuma migração.

Dica

Usar UseSeeding e UseAsyncSeeding é a maneira recomendada de semear o banco de dados com dados iniciais ao trabalhar com o EF Core.

Esses métodos podem ser configurados na etapade configuração de opções . Aqui está 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 a partir do método EnsureCreated, e UseAsyncSeeding é chamado a partir do método EnsureCreatedAsync. Ao usar esse recurso, é recomendável implementar métodos UseSeeding e UseAsyncSeeding usando lógica semelhante, mesmo que o código usando o EF seja assíncrono. As ferramentas do EF Core atualmente dependem da versão síncrona do método e não semearão 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.SaveChangesAsync() antes que a lógica principal do aplicativo comece a ser executada. Recomenda-se usar UseSeeding e UseAsyncSeeding para esse fim, no entanto, às vezes usar esses métodos não é uma boa solução. Um cenário de exemplo é quando a semeadura requer o uso de dois contextos diferentes em uma transação. Abaixo está um exemplo de código executando inicialização personalizada no aplicativo diretamente:

using (var context = new DataSeedingContext())
{
    await context.Database.EnsureCreatedAsync();

    var testBlog = await context.Blogs.FirstOrDefaultAsync(b => b.Url == "http://test.com");
    if (testBlog == null)
    {
        context.Blogs.Add(new Blog { Url = "http://test.com" });
        await context.SaveChangesAsync();
    }
}

Advertência

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 do banco de dados.

Dependendo das restrições de sua implantação, o código de inicialização pode ser executado de diferentes maneiras:

  • Executando o aplicativo de inicialização localmente
  • Implantar o aplicativo de inicialização com o aplicativo principal, invocar a rotina de inicialização e desabilitar ou remover o aplicativo de inicialização.

Isso geralmente pode ser automatizado usando perfis de publicação.

Modelar dados gerenciados

Os dados também podem ser associados a um tipo de entidade como parte da configuração do modelo. Em seguida, as migrações do EF Core podem calcular automaticamente quais operações de inserção, atualização ou exclusão precisam ser aplicadas ao atualizar o banco de dados para uma nova versão do modelo.

Advertência

As migrações só consideram as alterações de modelo ao determinar qual operação deve ser executada para colocar os dados gerenciados no estado desejado. Assim, quaisquer alterações nos dados realizadas fora das migrações podem ser perdidas ou causar um erro.

Como 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 quaisquer propriedades no estado de sombra (por exemplo, a entidade de junção de LanguageCountry abaixo), uma classe anônima pode 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 forma 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 tiverem sido adicionados ao modelo, migrações devem ser usadas para aplicar as alterações.

Dica

Se você precisar aplicar migrações como parte de uma implantação automatizada, poderá criar um de script SQL que possa ser visualizado antes da execução.

Como alternativa, você pode usar context.Database.EnsureCreatedAsync() 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. Observe que, se o banco de dados já existir, EnsureCreatedAsync() não atualizará o esquema nem os dados gerenciados no banco de dados. Para bancos de dados relacionais, você não deve chamar EnsureCreatedAsync() se planeja usar Migrações.

Observação

Preencher a base de dados usando o método HasData era anteriormente referido como "população de dados". Essa nomenclatura define expectativas incorretas, pois o recurso tem várias limitações e só é apropriado para tipos específicos de dados. É por isso que decidimos renomeá-lo para "modelo de dados gerenciados". UseSeeding e UseAsyncSeeding métodos devem ser usados para a semeadura 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. Isto 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 detetar 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, CEPs.

Se o seu cenário incluir qualquer um dos seguintes, é recomendável usar UseSeeding e UseAsyncSeeding métodos descritos na primeira seção:

  • Dados temporários para testes
  • Dados que dependem do estado do banco de dados
  • Dados de grande volume (os dados iniciais são capturados em instantâneos de migração e dados de grande volume podem levar rapidamente 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 identidade
  • Dados que requerem transformação específica (que não são processados por conversões de valor), como o hashing de algumas senhas.
  • Dados que exigem chamadas para API externa, como funções de Identidade Principal ASP.NET e criação de usuários
  • Dados que não são fixos e determinísticos, como a semeadura para DateTime.Now.

Personalização da migração manual

Quando uma migração é adicionada, as alterações nos dados especificados com HasData são transformadas em chamadas para InsertData(), UpdateData()e DeleteData(). Uma maneira de contornar algumas das limitações do 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 }
    });