Datenseeding
Bei der Dateineinspeisung handelt es sich um den Prozess, bei dem eine Datenbank mit einem anfänglichen Datensatz gefüllt wird.
Es gibt verschiedene Möglichkeiten, dies in EF Core zu erreichen:
- Konfigurationsoptionen für Datenseeding (
UseSeeding
) - Benutzerdefinierte Initialisierungslogik
- Modellseitig verwaltete Daten (
HasData
) - Manuelle Migrationsanpassung
Konfigurationsoptionen: Methoden UseSeeding
und UseAsyncSeeding
In EF 9 wurden die Methoden UseSeeding
und UseAsyncSeeding
eingeführt, mit denen die Datenbank komfortabel per Seeding mit anfänglichen Daten gefüllt werden kann. Diese Methoden dienen dazu, die Verwendung benutzerdefinierter Initialisierungslogik (siehe unten) zu verbessern. Durch sie kann der gesamte Code für das Datenseeding an einem einzelnen Ort platziert werden. Darüber hinaus ist der Code innerhalb der Methoden UseSeeding
und UseAsyncSeeding
durch den Migrationssperrmechanismus geschützt, um Parallelitätsproblemen vorzubeugen.
Die neuen Seedingmethoden werden im Rahmen des Vorgangs EnsureCreated
sowie im Rahmen von Migrate
und im Rahmen des Befehls dotnet ef database update
aufgerufen, auch wenn keine Modelländerungen vorhanden sind und keine Migrationen angewendet wurden.
Tipp
Es wird empfohlen, bei der Arbeit mit EF Core UseSeeding
und UseAsyncSeeding
zu verwenden, um die Datenbank per Seeding mit anfänglichen Daten zu füllen.
Diese Methoden können im Schritt zum Konfigurieren der Optionen eingerichtet werden. Hier ist ein Beispiel:
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);
}
});
Hinweis
UseSeeding
wird über die Methode EnsureCreated
und UseAsyncSeeding
über die Methode EnsureCreatedAsync
aufgerufen. Bei Verwendung dieses Features wird empfohlen, sowohl die Methode UseSeeding
als auch die Methode UseAsyncSeeding
mithilfe ähnlicher Logik zu implementieren, auch wenn der Code, der EF verwendet, asynchron ist. EF Core-Tools basieren derzeit auf der synchronen Version der Methode und führen kein ordnungsgemäßes Seeding der Datenbank durch, wenn die Methode UseSeeding
nicht implementiert wird.
Benutzerdefinierte Initialisierungslogik
Eine einfache und leistungsstarke Möglichkeit zum Ausführen von Datenseeding besteht darin, DbContext.SaveChanges()
zu verwenden, bevor die Hauptanwendungslogik mit der Ausführung beginnt. Es wird empfohlen, für diesen Zweck UseSeeding
und UseAsyncSeeding
zu verwenden. Manchmal ist die Verwendung dieser Methoden aber keine gute Lösung. Ein Beispiel wäre etwa ein Szenario, in dem für das Seeding zwei unterschiedliche Kontexte in einer einzelnen Transaktion verwendet werden müssen. Nachfolgend finden Sie ein Codebeispiel, in dem die benutzerdefinierte Initialisierung direkt in der Anwendung ausgeführt wird:
using (var context = new DataSeedingContext())
{
context.Database.EnsureCreated();
var testBlog = context.Blogs.FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Blogs.Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
}
Warnung
Der Seedingcode sollte nicht Teil der normalen App-Ausführung sein, da er zu Parallelitätsproblemen führen kann, wenn mehrere Instanzen ausgeführt werden. Außerdem müsste die App über die Berechtigung zum Ändern des Datenbankschemas verfügen.
Abhängig von den Einschränkungen Ihrer Bereitstellung kann der Initialisierungscode auf unterschiedliche Weise ausgeführt werden:
- Lokales Ausführen der Initialisierungs-App
- Bereitstellen der Initialisierungs-App mit der Haupt-App, Aufrufen der Initialisierungsroutine und Deaktivieren oder Entfernen der Initialisierungs-App
Dieser Vorgang kann in der Regel mithilfe von Veröffentlichungsprofilen automatisiert werden.
Modellseitig verwaltete Daten
Daten können auch als Teil der Modellkonfiguration mit einem Entitätstyp verknüpft werden. EF Core-Migrationen können dann automatisch berechnen, welche Einfüge-, Aktualisierungs- oder Löschvorgänge beim Durchführen eines Upgrades für die Datenbank auf eine neue Modellversion angewendet werden müssen.
Warnung
Bei Migrationen werden Modelländerungen nur berücksichtigt, wenn bestimmt wird, welcher Vorgang ausgeführt werden muss, um die verwalteten Daten in den gewünschten Zustand zu versetzen. Daher können Änderungen an den außerhalb von Migrationen ausgeführten Daten verloren gehen oder einen Fehler verursachen.
Dadurch werden beispielsweise verwaltete Daten für ein Land (Country
) in OnModelCreating
konfiguriert:
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" });
});
Um Entitäten hinzuzufügen, die über eine Beziehung verfügen, müssen die Fremdschlüsselwerte angegeben werden:
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 });
Bei der Verwaltung von Daten für n:n-Navigationen muss die Joinentität explizit konfiguriert werden. Wenn der Entitätstyp über Eigenschaften im Schattenzustand verfügt (z. B. die Joinentität LanguageCountry
unten), kann eine anonyme Klasse verwendet werden, um die Werte bereitzustellen:
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 });
});
});
Nicht eigenständige Entitätstypen können auf ähnliche Weise konfiguriert werden:
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 });
Weitere Kontexte finden Sie im vollständigen Beispielprojekt.
Nachdem die Daten dem Modell hinzugefügt wurden, sollten Migrationen verwendet werden, um die Änderungen anzuwenden.
Tipp
Wenn Sie Migrationen als Teil einer automatisierten Bereitstellung anwenden müssen, können Sie ein SQL-Skript erstellen, das vor der Ausführung in der Vorschau angezeigt werden kann.
Alternativ können Sie context.Database.EnsureCreated()
verwenden, um eine neue Datenbank mit den verwalteten Daten zu erstellen – z. B. für eine Testdatenbank oder bei Verwendung des In-Memory-Anbieters oder einer nicht relationalen Datenbank. Ist die Datenbank bereits vorhanden, aktualisiert EnsureCreated()
weder das Schema noch die verwalteten Daten in der Datenbank. Bei relationalen Datenbanken sollten Sie EnsureCreated()
nicht aufrufen, wenn Sie Migrationen verwenden möchten.
Hinweis
Das Auffüllen der Datenbank mithilfe der Methode HasData
wurde früher als Datenseeding bezeichnet. Diese Benennung weckt jedoch falsche Erwartungen, da das Feature eine Reihe von Einschränkungen aufweist und nur für bestimmte Datentypen geeignet ist. Deshalb haben wir beschlossen, es in „modellseitig verwaltete Daten“ umzubenennen. Die Methoden UseSeeding
und UseAsyncSeeding
sollten für universelles Datenseeding verwendet werden.
Einschränkungen modellseitig verwalteter Daten
Diese Art von Daten wird durch Migrationen verwaltet, und das Skript zum Aktualisieren der Daten, die sich bereits in der Datenbank befinden, muss generiert werden, ohne eine Verbindung mit der Datenbank herzustellen. Dies erzwingt einige Einschränkungen:
- Der Primärschlüsselwert muss angegeben werden, auch wenn er normalerweise von der Datenbank generiert wird. Er wird verwendet, um Datenänderungen zwischen Migrationen zu erkennen.
- Zuvor eingefügte Daten werden entfernt, wenn der Primärschlüssel in irgendeiner Weise verändert wird.
Daher ist dieses Feature für statische Daten am nützlichsten, die nicht außerhalb von Migrationen geändert werden sollen, und hängt nicht von anderen Elementen in der Datenbank ab, z. B. Postleitzahlen.
Wenn Ihr Szenario einen der folgenden Datentypen umfasst, wird empfohlen, die im ersten Abschnitt beschriebenen Methoden UseSeeding
und UseAsyncSeeding
zu verwenden:
- Temporäre Daten für Tests
- Daten, die vom Datenbankstatus abhängen
- Daten, die groß sind (Seedingdaten werden in Migrationsmomentaufnahmen erfasst, und große Daten können schnell zu riesigen Dateien und beeinträchtigter Leistung führen.)
- Daten, die von der Datenbank generiert werden müssen, einschließlich Entitäten, die alternative Schlüssel als Identität verwenden
- Daten, die eine benutzerdefinierte Transformation erfordern (die von Wertkonvertierungen nicht behandelt werden), z. B. Kennworthashing in irgend einer Form
- Daten, die Aufrufe an externe API erfordern, z. B. ASP.NET Core Identity-Rollen und -Benutzererstellung
Manuelle Migrationsanpassung
Wenn eine Migration hinzugefügt wird, werden die Änderungen an den mit HasData
angegebenen Daten in Aufrufe von InsertData()
, UpdateData()
und DeleteData()
transformiert. Eine Möglichkeit, um einige der Einschränkungen von HasData
zu umgehen, besteht darin, stattdessen diese Aufrufe oder benutzerdefinierte Vorgänge der Migration manuell hinzuzufügen.
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 }
});