Condividi tramite


Seeding dei dati

Il seeding dei dati è il processo di popolamento di un database con un set iniziale di dati.

È possibile eseguire questa operazione in EF Core in diversi modi:

Opzioni UseSeeding e UseAsyncSeeding metodi di configurazione

EF 9 ha introdotto UseSeeding i metodi e UseAsyncSeeding , che offrono un modo pratico per eseguire il seeding del database con i dati iniziali. Questi metodi mirano a migliorare l'esperienza di utilizzo della logica di inizializzazione personalizzata (spiegata di seguito). Forniscono una posizione chiara in cui è possibile inserire tutto il codice di seeding dei dati. Inoltre, il codice all'interno UseSeeding e UseAsyncSeeding i metodi sono protetti dal meccanismo di blocco della migrazione per evitare problemi di concorrenza.

I nuovi metodi di seeding vengono chiamati come parte dell'operazione EnsureCreatedMigrate e dotnet ef database update comando, anche se non sono state apportate modifiche al modello e non sono state applicate migrazioni.

Suggerimento

L'uso UseSeeding di e UseAsyncSeeding è il modo consigliato per eseguire il seeding del database con i dati iniziali quando si usa EF Core.

Questi metodi possono essere configurati nel passaggio di configurazione delle opzioni. Ecco un esempio:

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);
            }
        });

Nota

UseSeeding viene chiamato dal EnsureCreated metodo e UseAsyncSeeding viene chiamato dal EnsureCreatedAsync metodo . Quando si usa questa funzionalità, è consigliabile implementare entrambi UseSeeding i metodi e UseAsyncSeeding usando logica simile, anche se il codice che usa Entity Framework è asincrono. Gli strumenti di EF Core attualmente si basano sulla versione sincrona del metodo e non eseguiranno correttamente il seeding del database se il UseSeeding metodo non è implementato.

Logica di inizializzazione personalizzata

Un modo semplice e potente per eseguire il seeding dei dati consiste nell'usare DbContext.SaveChangesAsync() prima che la logica principale dell'applicazione inizi l'esecuzione. È consigliabile usare UseSeeding e UseAsyncSeeding a tale scopo, tuttavia a volte l'uso di questi metodi non è una soluzione valida. Uno scenario di esempio è quando il seeding richiede l'uso di due contesti diversi in una transazione. Di seguito è riportato un esempio di codice che esegue direttamente l'inizializzazione personalizzata nell'applicazione:

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();
    }
}

Avviso

Il codice di seeding non deve far parte della normale esecuzione dell'app perché ciò può causare problemi di concorrenza quando sono in esecuzione più istanze e richiederebbe anche all'app l'autorizzazione per modificare lo schema del database.

A seconda dei vincoli della distribuzione, il codice di inizializzazione può essere eseguito in modi diversi:

  • Esecuzione dell'app di inizializzazione in locale
  • Distribuzione dell'app di inizializzazione con l'app principale, richiamando la routine di inizializzazione e disabilitando o rimuovendo l'app di inizializzazione.

Questo può in genere essere automatizzato usando i profili di pubblicazione.

Modellare i dati gestiti

I dati possono anche essere associati a un tipo di entità come parte della configurazione del modello. Le migrazioni di EF Core possono quindi calcolare automaticamente le operazioni di inserimento, aggiornamento o eliminazione da applicare durante l'aggiornamento del database a una nuova versione del modello.

Avviso

Le migrazioni considerano solo le modifiche del modello quando si determina quale operazione deve essere eseguita per ottenere i dati gestiti nello stato desiderato. Di conseguenza, eventuali modifiche apportate ai dati eseguiti all'esterno delle migrazioni potrebbero andare perse o causare un errore.

Ad esempio, verranno configurati i dati gestiti per un Country in 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" });
});

Per aggiungere entità con una relazione, è necessario specificare i valori di chiave esterna:

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 });

Quando si gestiscono i dati per spostamenti molti-a-molti, l'entità join deve essere configurata in modo esplicito. Se il tipo di entità ha proprietà nello stato shadow ,ad esempio l'entità LanguageCountry join seguente, è possibile usare una classe anonima per fornire i valori:

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 });
            });
});

I tipi di entità di proprietà possono essere configurati in modo simile:

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 });

Per altre informazioni di contesto, vedere il progetto di esempio completo.

Dopo aver aggiunto i dati al modello, è necessario usare le migrazioni per applicare le modifiche.

Suggerimento

Se è necessario applicare le migrazioni come parte di una distribuzione automatizzata, è possibile creare uno script SQL che può essere visualizzato in anteprima prima dell'esecuzione.

In alternativa, è possibile usare context.Database.EnsureCreatedAsync() per creare un nuovo database contenente i dati gestiti, ad esempio per un database di test o quando si usa il provider in memoria o qualsiasi database non relazionale. Si noti che se il database esiste già, EnsureCreatedAsync() non aggiornerà né lo schema né i dati gestiti nel database. Per i database relazionali non è consigliabile chiamare EnsureCreatedAsync() se si prevede di usare le migrazioni.

Nota

Popolamento del database tramite il HasData metodo usato per essere definito "seeding dei dati". Questa denominazione imposta aspettative non corrette, poiché la funzionalità presenta una serie di limitazioni ed è appropriata solo per tipi specifici di dati. Ecco perché abbiamo deciso di rinominarlo in "dati gestiti dal modello". UseSeeding i metodi e UseAsyncSeeding devono essere usati per il seeding dei dati per utilizzo generico.

Limitazioni dei dati gestiti dal modello

Questo tipo di dati viene gestito dalle migrazioni e lo script per aggiornare i dati già presenti nel database deve essere generato senza connettersi al database. Ciò impone alcune restrizioni:

  • Il valore della chiave primaria deve essere specificato anche se in genere viene generato dal database. Verrà usato per rilevare le modifiche ai dati tra le migrazioni.
  • I dati inseriti in precedenza verranno rimossi se la chiave primaria viene modificata in qualsiasi modo.

Questa funzionalità è quindi più utile per i dati statici che non devono cambiare all'esterno delle migrazioni e non dipendono da altri elementi nel database, ad esempio i codici ZIP.

Se lo scenario include una delle opzioni seguenti, è consigliabile usare UseSeeding i metodi e UseAsyncSeeding descritti nella prima sezione:

  • Dati temporanei per i test
  • Dati che dipendono dallo stato del database
  • Dati di grandi dimensioni (i dati di seeding vengono acquisiti negli snapshot di migrazione e i dati di grandi dimensioni possono portare rapidamente a file di grandi dimensioni e prestazioni ridotte).
  • Dati che devono essere generati dal database con valori chiave, incluse le entità che usano chiavi alternative come identità
  • Dati che richiedono una trasformazione personalizzata (non gestita dalle conversioni di valori), ad esempio alcuni hash delle password
  • Dati che richiedono chiamate all'API esterna, ad esempio ASP.NET ruoli di identità di base e creazione di utenti
  • Dati non fissi e deterministici, ad esempio il seeding in DateTime.Now.

Personalizzazione della migrazione manuale

Quando viene aggiunta una migrazione, le modifiche ai dati specificati con HasData vengono trasformate in chiamate a InsertData(), UpdateData()e DeleteData(). Un modo per aggirare alcune delle limitazioni di HasData consiste nell'aggiungere manualmente queste chiamate o operazioni personalizzate alla migrazione.

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 }
    });