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 / UseAsyncSeedingsão invocados durante EnsureCreated/EnsureCreatedAsync e após a aplicação das migrações (por exemplo,/MigrateMigrateAsync , dotnet ef database update, e os pacotes de migração). As ferramentas e bundles do EF Core atualmente dependem do delegado síncrono, por isso implementa UseSeeding sempre mesmo que a tua aplicação normalmente use APIs assíncronas.

Comportamento de implantação

UseSeeding e UseAsyncSeeding executam apenas quando o EF Core realiza uma operação de inicialização ou migração da base de dados. Escolha um mecanismo de implementação em conformidade:

Operation Delegado de cabeça de série invocado
EnsureCreated ou Migrate UseSeeding
EnsureCreatedAsync ou MigrateAsync UseAsyncSeeding
dotnet ef database update ou Update-Database UseSeeding
Feixe de migração UseSeeding
Script SQL executado por uma ferramenta SQL externa None

Para implementação automatizada que deve ser executada UseSeeding, utilize um pacote de migração ou um processo dedicado de inicialização. As ferramentas e bundles do EF Core invocam o delegado síncrono, por isso implementa UseSeeding sempre mesmo que a aplicação normalmente use APIs assíncronas. Use um script SQL quando for necessária revisão ou execução de DBA e os dados seed são representados por operações de migração. Veja Aplicar Migrações para os compromissos.

O seeding também ocorre após um downgrade de migração. Se a aplicação suportar o downgrade para uma migração que não contenha todas as tabelas usadas pelo código de seeding, verifique se o esquema necessário existe antes de o consultar. Isto é especialmente importante ao reverter todas as migrações ao direcionar 0.

As aplicações Aspire podem coordenar a execução local de migrações e publicar pacotes ou scripts de migração com a integração de migrações Aspire EF Core.

Lógica de inicialização personalizada

Uma maneira simples e poderosa de executar a propagação de dados é usar 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 tenham uma relação, é necessário especificar os valores de chave estrangeira.

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.

HasData as alterações são convertidas em InsertData, UpdateData, e DeleteData operações quando uma migração é estruturada. A chamada Migrate não inspeciona independentemente a configuração atual HasData . Depois de alterar os dados geridos pelo modelo, adicione e implemente uma nova migração.

Dica

Para implementação automatizada, utilize um pacote de migração. Use um script SQL quando for necessário pré-visualizar ou alterar antes da execução.

Como alternativa, você pode usar 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 requerem chamadas para API externa, como funções de identidade ASP.NET Core e criação de utilizadores
  • 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 }
    });

Estas operações são apropriadas quando os valores e chaves são fixos quando a migração é escrita. Eles não consultam o estado atual da base de dados. Veja Operações de dados nas migrações para exemplos de InsertData, UpdateData, DeleteData, e transformações SQL específicas do fornecedor.