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:

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

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 EnsureCreated, 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 / UseAsyncSeedingsão invocados durante EnsureCreated/EnsureCreatedAsync e depois que as migrações são aplicadas (por exemplo,/MigrateMigrateAsync e dotnet ef database updatepacotes de migração). Atualmente, as ferramentas e os pacotes do EF Core dependem do delegado síncrono, portanto, sempre implemente UseSeeding mesmo que seu aplicativo normalmente use APIs assíncronas.

Comportamento de implantação

UseSeeding e UseAsyncSeeding executado somente quando o EF Core executa uma operação de inicialização ou migração de banco de dados. Escolha um mecanismo de implantação adequadamente:

Operation Delegado de propagação invocado
EnsureCreated ou Migrate UseSeeding
EnsureCreatedAsync ou MigrateAsync UseAsyncSeeding
dotnet ef database update ou Update-Database UseSeeding
Pacote de migração UseSeeding
Script SQL executado por uma ferramenta SQL externa None

Para implantação automatizada que deve ser executada UseSeeding, use um pacote de migração ou um processo de inicialização dedicado. As ferramentas e os pacotes do EF Core invocam o delegado síncrono, portanto, sempre implemente UseSeeding mesmo que o aplicativo normalmente use APIs assíncronas. Use um script SQL quando a revisão ou a execução do DBA for necessária e os dados de semente forem representados por operações de migração. Consulte a aplicação de migrações para as compensações.

A propagação também é executada após um downgrade de migração. Se o aplicativo der suporte ao downgrade para uma migração que não contenha todas as tabelas usadas pelo código de propagação, verifique se o esquema necessário existe antes de consultá-lo. Isso é especialmente importante ao reverter todas as migrações direcionando 0.

Os aplicativos Aspire podem coordenar a execução da migração local e publicar pacotes de migração ou scripts com a integração de migrações do 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 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 semeadura 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())
{
    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();
    }
}

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 de inserção, atualização ou exclusão 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.

HasData as alterações são convertidas InsertDataem , UpdateDatae DeleteData operações quando uma migração é scaffolded. A chamada Migrate não inspeciona independentemente a configuração atual HasData . Depois de alterar os dados gerenciados pelo modelo, adicione e implante uma nova migração.

Dica

Para implantação automatizada, use um pacote de migração. Use um script SQL quando ele precisar ser visualizado ou alterado 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. 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 o banco de dados usando o método HasData costumava ser chamado de "semeadura 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 funções de identidade ASP.NET Core 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 }
    });

Essas operações são apropriadas quando os valores e as chaves são corrigidos quando a migração é gravada. Eles não consultam o estado atual do banco de dados. Consulte operações de dados em migrações para obter exemplos de InsertDataUpdateDataDeleteDatatransformações SQL específicas do provedor e do provedor.