Dela via


Data Seeding

Datasådd är processen att fylla i en databas med en första uppsättning data.

Det finns flera sätt att åstadkomma detta i EF Core:

Konfigurationsalternativ UseSeeding och UseAsyncSeeding metoder

EF 9 introducerade UseSeeding och UseAsyncSeeding metoder, vilket ger ett bekvämt sätt att så databasen med inledande data. Dessa metoder syftar till att förbättra upplevelsen av att använda anpassad initieringslogik (förklaras nedan). De ger en tydlig plats där all data seeding-kod kan placeras. Dessutom skyddas koden i UseSeeding- och UseAsyncSeeding-metoderna av migreringslåsningsmekanismen för att förhindra samtidighetsproblem.

De nya seeding-metoderna anropas som en del av EnsureCreated åtgärd, Migrate och dotnet ef database update kommando, även om det inte finns några modelländringar och inga migreringar har tillämpats.

Tips

Att använda UseSeeding och UseAsyncSeeding är det rekommenderade sättet att seeda databasen med initiala data när du arbetar med EF Core.

Dessa metoder kan konfigureras i konfigurationssteget alternativ. Här är ett exempel:

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

Notis

UseSeeding anropas från metoden EnsureCreated och UseAsyncSeeding anropas från metoden EnsureCreatedAsync. När du använder den här funktionen rekommenderar vi att du implementerar både UseSeeding och UseAsyncSeeding metoder med liknande logik, även om koden som använder EF är asynkron. EF Core-verktygen förlitar sig för närvarande på denna synkrona version av metoden och kommer därför inte att fylla databasen korrekt om metoden UseSeeding inte implementeras.

Anpassad initieringslogik

Ett enkelt och kraftfullt sätt att utföra datasådd är att använda DbContext.SaveChangesAsync() innan huvudprogramlogik börjar köras. Vi rekommenderar att du använder UseSeeding och UseAsyncSeeding för detta ändamål, men ibland är det inte en bra lösning att använda dessa metoder. Ett exempelscenario är när seeding kräver att två olika kontexter används i en transaktion. Nedan visas ett kodexempel som utför anpassad initiering i programmet direkt:

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

Varning

Seeding-koden bör inte ingå i den normala appkörningen eftersom detta kan orsaka samtidighetsproblem när flera instanser körs och kräver också att appen har behörighet att ändra databasschemat.

Beroende på begränsningarna i distributionen kan initieringskoden köras på olika sätt:

  • Köra initieringsappen lokalt
  • Distribuera initieringsappen med huvudappen, anropa initieringsrutinen och inaktivera eller ta bort initieringsappen.

Detta kan vanligtvis automatiseras med hjälp av publiceringsprofiler.

Data som hanteras av modellen

Data kan också associeras med en entitetstyp som en del av modellkonfigurationen. Sedan kan EF Core migreringar automatiskt beräkna vilka åtgärder för att infoga, uppdatera eller ta bort som måste tillämpas när databasen uppgraderas till en ny version av modellen.

Varning

Migreringar tar endast hänsyn till modelländringar när du fastställer vilken åtgärd som ska utföras för att få hanterade data i önskat tillstånd. Därför kan eventuella ändringar av data som utförs utanför migreringen gå förlorade eller orsaka ett fel.

Detta konfigurerar till exempel hanterade data för en Country i 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" });
});

För att lägga till entiteter som har en relation måste värdena för extern nyckel anges.

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

När du hanterar data för många-till-många-navigeringar måste kopplingsentiteten konfigureras explicit. Om entitetstypen har några egenskaper i skuggtillstånd (t.ex. den LanguageCountry kopplingsentiteten nedan) kan en anonym klass användas för att ange värdena:

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

Ägda entitetstyper kan konfigureras på ett liknande sätt:

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

Mer kontext finns i fullständiga exempelprojektet.

När data har lagts till i modellen bör migreringar användas för att tillämpa ändringarna.

Tips

Om du behöver tillämpa migreringar som en del av en automatiserad distribution kan du skapa ett SQL-skript som kan förhandsgranskas före körning.

Du kan också använda context.Database.EnsureCreatedAsync() för att skapa en ny databas som innehåller hanterade data, till exempel för en testdatabas eller när du använder den minnesinterna providern eller en icke-relationell databas. Observera att om databasen redan finns uppdaterar EnsureCreatedAsync() varken schemat eller hanterade data i databasen. För relationsdatabaser bör du inte anropa EnsureCreatedAsync() om du planerar att använda migreringar.

Notera

Fylla i databasen med hjälp av den HasData-metoden brukade kallas "data seeding". Den här namngivningen anger felaktiga förväntningar eftersom funktionen har ett antal begränsningar och endast är lämplig för specifika typer av data. Därför bestämde vi oss för att byta namn på den till "modellhanterade data". UseSeeding och UseAsyncSeeding metoder bör användas för generell datasådd.

Begränsningar för modellhanterade data

Den här typen av data hanteras av migreringar och skriptet för att uppdatera data som redan finns i databasen måste genereras utan att ansluta till databasen. Detta medför vissa begränsningar:

  • Det primära nyckelvärdet måste anges även om det vanligtvis genereras av databasen. Den används för att identifiera dataändringar mellan migreringar.
  • Tidigare infogade data tas bort om primärnyckeln ändras på något sätt.

Därför är den här funktionen mest användbar för statiska data som inte förväntas ändras utanför migreringar och som inte är beroende av något annat i databasen, till exempel postnummer.

Om ditt scenario innehåller något av följande rekommenderar vi att du använder UseSeeding och UseAsyncSeeding metoder som beskrivs i det första avsnittet:

  • Tillfälliga data för testning
  • Data som är beroende av databastillstånd
  • Data som är stora (seeding-data samlas in i ögonblicksbilder av migrering, och stora data kan snabbt leda till enorma filer och försämrad prestanda).
  • Data som behöver nyckelvärden som ska genereras av databasen, inklusive entiteter som använder alternativa nycklar som identitet
  • Data som kräver anpassad transformering (som inte hanteras av värdekonverteringar), till exempel viss lösenordshashing
  • Data som kräver anrop till ett externt API, till exempel skapande av ASP.NET Core Identity-roller och användare
  • Data som inte är fasta och deterministiska, som exempelvis fröinställningar till DateTime.Now.

Manuell migreringsanpassning

När en migrering läggs till omvandlas ändringarna i de data som anges med HasData till anrop av InsertData(), UpdateData()och DeleteData(). Ett sätt att kringgå vissa av begränsningarna i HasData är att manuellt lägga till dessa anrop eller anpassade operationer till migreringen i stället.

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