Explizite Nachverfolgung von Entitäten
Jede DbContext-Instanz verfolgt Änderungen nach, die an Entitäten vorgenommen wurden. Diese nachverfolgten Entitäten bestimmen wiederum die Änderungen an der Datenbank, wenn SaveChanges aufgerufen wird.
Entity Framework Core (EF Core)-Änderungsnachverfolgung funktioniert am besten, wenn dieselbe DbContext-Instanz verwendet wird, um Entitäten abzufragen und sie durch Aufrufen von SaveChanges zu aktualisieren. Der Grund hierfür ist, dass EF Core den Status von abgefragten Entitäten automatisch nachverfolgt und dann alle Änderungen erkennt, die an diesen Entitäten vorgenommen werden, wenn SaveChanges aufgerufen wird. Dieser Ansatz wird in der Änderungsnachverfolgung in EF Corebehandelt.
Tipp
In diesem Dokument wird davon ausgegangen, dass Entitätszustände und die Grundlagen der EF Core-Änderungsnachverfolgung verstanden werden. Weitere Informationen zu diesen Themen finden Sie unter Änderungsnachverfolgung in EF Core.
Tipp
Sie können den gesamten Code in dieser Dokumentation ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.
Tipp
Der Einfachheit halber werden in diesem Dokument synchrone Methoden wie z. B. SaveChanges anstatt ihrer asynchronen Entsprechungen wie SaveChangesAsync verwendet und referenziert. Das Aufrufen und Warten auf die asynchrone Methode kann ersetzt werden, sofern nicht anders angegeben.
Einführung
Entitäten können explizit an DbContext "angefügt" werden, sodass der Kontext diese Entitäten nachverfolgt. Dies ist in erster Linie nützlich, wenn:
- Erstellen neuer Entitäten, die in die Datenbank eingefügt werden.
- Erneutes Anfügen getrennter Entitäten, die zuvor von einer anderen DbContext-Instanz abgefragt wurden.
Die erste davon wird von den meisten Anwendungen benötigt und wird in erster Linie von den DbContext.Add-Methoden behandelt.
Die zweite wird nur von Anwendungen benötigt, die Entitäten oder ihre Beziehungen ändern, während die Entitäten nicht nachverfolgt werden. Beispielsweise kann eine Webanwendung Entitäten an den Webclient senden, in dem der Benutzer Änderungen vornimmt und die Entitäten zurücksendet. Diese Entitäten werden als "getrennt" bezeichnet, da sie ursprünglich von einem DbContext abgefragt wurden, aber dann vom Kontext getrennt wurden, als sie an den Client gesendet wurden.
Die Webanwendung muss diese Entitäten jetzt erneut anfügen, damit sie wieder nachverfolgt werden und die vorgenommenen Änderungen durchgeführt werden können, sodass SaveChanges entsprechende Aktualisierungen an der Datenbank vornehmen kann. Dies wird in erster Linie von den Methoden DbContext.Attach und DbContext.Update behandelt.
Tipp
Das Anfügen von Entitäten an dieselbe DbContext-Instanz, von der sie abgefragt wurden, sollte normalerweise nicht erforderlich sein. Führen Sie keine No-Tracking-Abfrage aus, und fügen dann die zurückgegebenen Entitäten an denselben Kontext an. Das ist langsamer als die Verwendung einer Nachverfolgungsabfrage und kann auch zu Problemen wie fehlenden Schatteneigenschaftswerten führen, wodurch es schwieriger wird, sie richtig durchzuführen.
Generierte und explizite Schlüsselwerte
Standardmäßig werden ganzzahlige und GUID-Schlüsseleigenschaften so konfiguriert, dass automatisch generierte Schlüsselwerte verwenden. Dies hat einen großen Vorteil für die Änderungsnachverfolgung: Ein nicht festgelegter Schlüsselwert gibt an, dass die Entität „neu“ist. Mit „neu“ meinen wir, dass sie noch nicht in die Datenbank eingefügt wurde.
In den folgenden Abschnitten werden zwei Modelle verwendet. Das erste ist so konfiguriert, dass keine generierten Schlüsselwerte verwendet werden:
public class Blog
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
}
Nicht generierte (d. h. explizit festgelegte) Schlüsselwerte werden zuerst in jedem Beispiel angezeigt, da alles sehr explizit und einfach zu befolgen ist. Anschließend folgt ein Beispiel, in dem generierte Schlüsselwerte verwendet werden:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<Post> Posts { get; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int? BlogId { get; set; }
public Blog Blog { get; set; }
}
Beachten Sie, dass die Schlüsseleigenschaften in diesem Modell hier keine zusätzliche Konfiguration benötigen, da die Verwendung generierter Schlüsselwerte der Standard für einfache ganzzahlige Schlüssel ist.
Einfügen neuer Entitäten
Explizite Schlüsselwerte
Eine Entität muss im Added
-Zustand nachverfolgt werden, der von SaveChanges eingefügt werden soll. Entitäten werden in der Regel in den Zustand Hinzugefügt gesetzt, indem sie einen von DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync oder die entsprechenden Methoden für DbSet<TEntity> aufrufen.
Tipp
Diese Methoden funktionieren alle im Kontext der Änderungsnachverfolgung auf die gleiche Weise. Weitere Informationen finden Sie unter Zusätzliche Features zur Änderungsnachverfolgung.
So beginnen Sie beispielsweise mit der Nachverfolgung eines neuen Blogs:
context.Add(
new Blog { Id = 1, Name = ".NET Blog", });
Das Überprüfen der Debugansicht der Änderungsverfolgung nach diesem Aufruf zeigt, dass der Kontext die neue Entität im Zustand Added
nachverfolgt:
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: []
Die Add-Methoden funktionieren jedoch nicht nur für eine einzelne Entität. Sie beginnen tatsächlich, ein ganzes Diagramm verwandter Entitäten nachzuverfolgen, wobei sie alle in den Zustand Added
versetzt werden. So fügen Sie z. B. einen neuen Blog und zugeordnete neue Beiträge ein:
context.Add(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Der Kontext verfolgt nun alle diese Entitäten wie Added
:
Blog {Id: 1} Added
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Added
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Beachten Sie, dass explizite Werte für die Id
-Schlüsseleigenschaften in den obigen Beispielen festgelegt wurden. Dies liegt daran, dass das Modell hier so konfiguriert wurde, dass explizit festgelegte Schlüsselwerte verwendet werden, anstatt automatisch generierter Schlüsselwerte. Wenn keine generierten Schlüssel verwendet werden, müssen die Schlüsseleigenschaften explizit vor dem Aufrufen von Add
festgelegt werden. Diese Schlüsselwerte werden dann eingefügt, wenn SaveChanges aufgerufen wird. Ein Beispiel sehen Sie bei der Verwendung von SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);
-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);
Alle diese Entitäten werden nach Abschluss von SaveChanges im Unchanged
-Zustand nachverfolgt, da diese Entitäten jetzt in der Datenbank vorhanden sind:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Generierte Schlüsselwerte
Wie bereits erwähnt, werden ganzzahlige und GUID-Schlüsseleigenschaften standardmäßig so konfiguriert, dass automatisch generierte Schlüsselwerte verwendet werden. Das bedeutet, dass die Anwendung keinen Schlüsselwert explizit festlegen darf. Um beispielsweise einen neuen Blog einzufügen und alle Beiträge mit generierten Schlüsselwerten zu veröffentlichen:
context.Add(
new Blog
{
Name = ".NET Blog",
Posts =
{
new Post
{
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Wie bei expliziten Schlüsselwerten verfolgt der Kontext jetzt alle diese Entitäten wie Added
:
Blog {Id: -2147482644} Added
Id: -2147482644 PK Temporary
Name: '.NET Blog'
Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
Id: -2147482637 PK Temporary
BlogId: -2147482644 FK Temporary
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
Id: -2147482636 PK Temporary
BlogId: -2147482644 FK Temporary
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: -2147482644}
Beachten Sie in diesem Fall, dass temporäre Schlüsselwerte für jede Entität generiert wurden. Diese Werte werden von EF Core verwendet, bis SaveChanges aufgerufen wird. An diesem Punkt werden echte Schlüsselwerte aus der Datenbank gelesen. Ein Beispiel sehen Sie bei der Verwendung von SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Nach Abschluss von SaveChanges wurden alle Entitäten mit ihren tatsächlichen Schlüsselwerten aktualisiert und werden im Unchanged
-Zustand nachverfolgt, da sie nun mit dem Status in der Datenbank übereinstimmen:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Dies ist genau derselbe Endzustand wie im vorherigen Beispiel, in dem explizite Schlüsselwerte verwendet wurden.
Tipp
Ein expliziter Schlüsselwert kann weiterhin festgelegt werden, auch wenn generierte Schlüsselwerte verwendet werden. EF Core versucht dann, diesen Schlüsselwert einzufügen. Einige Datenbankkonfigurationen, einschließlich SQL Server mit Identitätsspalten, unterstützen solche Einfügungen nicht und lösen daraufhin Ausnahmen aus (siehe diese Dokumente für eine Problemumgehung).
Anfügen vorhandener Entitäten
Explizite Schlüsselwerte
Von Abfragen zurückgegebene Entitäten werden im Zustand Unchanged
nachverfolgt. Der Zustand Unchanged
bedeutet, dass die Entität seit der Abfrage nicht geändert wurde. Eine getrennte Entität, die möglicherweise von einem Webclient in einer HTTP-Anforderung zurückgegeben wird, kann entweder mit DbContext.Attach, DbContext.AttachRange oder der entsprechenden Methoden von DbSet<TEntity> in diesen Zustand versetzt werden. So beginnen Sie beispielsweise mit der Nachverfolgung eines vorhandenen Blogs:
context.Attach(
new Blog { Id = 1, Name = ".NET Blog", });
Hinweis
In den folgenden Beispielen werden Entitäten explizit new
aus Gründen der Einfachheit erstellt. In der Regel stammen die Entitätsinstanzen aus einer anderen Quelle, z. B. deserialisiert von einem Client oder erstellt aus Daten in einem HTTP-Beitrag.
Das Überprüfen der Debugansicht der Änderungsverfolgung nach diesem Aufruf zeigt, dass die Entität im Unchanged
-Zustand nachverfolgt wird:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: []
Genau wie Add
legt Attach
tatsächlich ein ganzes Diagramm von verbundenen Entitäten auf den Zustand Unchanged
fest. So fügen Sie beispielsweise einen vorhandenen Blog und seine vorhandenen zugehörigen Beiträge an:
context.Attach(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Der Kontext verfolgt nun alle diese Entitäten wie Unchanged
:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Das Aufrufen von SaveChanges an diesem Punkt hat keine Auswirkung. Alle Entitäten werden als Unchanged
gekennzeichnet, sodass in der Datenbank nichts aktualisiert werden muss.
Generierte Schlüsselwerte
Wie bereits erwähnt, werden ganzzahlige und GUID-Schlüsseleigenschaften standardmäßig so konfiguriert, dass automatisch generierte Schlüsselwerte verwendet werden. Dies hat einen großen Vorteil beim Arbeiten mit getrennten Entitäten: Ein nicht festgelegter Schlüsselwert zeigt, dass die Entität noch nicht in die Datenbank eingefügt wurde. Auf diese Weise kann der Änderungstracker neue Entitäten automatisch erkennen und in den Added
-Zustand versetzen. Ziehen Sie z. B. das Anfügen dieses Diagramms eines Blogs und seinen Beiträgen in Betracht:
context.Attach(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
},
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
},
}
});
Der Blog hat einen Schlüsselwert von 1, der angibt, dass er bereits in der Datenbank vorhanden ist. Zwei der Beiträge verfügen auch über festgelegte Schlüsselwerte, der dritte jedoch nicht. EF Core sieht diesen Schlüsselwert als 0, den CLR-Standardwert für eine ganze Zahl. Dies führt dazu, dass EF Core die neue Entität als Added
anstelle von Unchanged
markiert:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
Id: -2147482636 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 includes many enhancements, including single file a...'
Title: 'Announcing .NET 5.0'
Blog: {Id: 1}
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Das Aufrufen von SaveChanges an diesem Punkt hat keinen Effekt auf die Unchanged
-Entitäten, fügt aber neue Entitäten in die Datenbank ein. Ein Beispiel sehen Sie bei der Verwendung von SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Das Wichtige, was man hier mitnehmen sollte, ist, dass EF Core mit generierten Schlüsselwerten automatisch neue von vorhandenen Entitäten in einem getrennten Diagramm unterscheiden kann. Kurz gesagt, wenn EF Core generierte Schlüssel verwendet, fügt EF Core immer eine Entität ein, wenn diese Entität keinen festgelegten Schlüsselwert hat.
Aktualisieren vorhandener Entitäten
Explizite Schlüsselwerte
DbContext.Update, DbContext.UpdateRange und die entsprechenden Methoden von DbSet<TEntity> verhalten sich genau wie die oben beschriebenen Attach
-Methoden, mit der Ausnahme, dass Entitäten anstelle des Modified
-Zustands in den Unchanged
-Zustand versetzt werden. Um z. B. mit dem Nachverfolgen eines vorhandenen Blogs als Modified
zu beginnen:
context.Update(
new Blog { Id = 1, Name = ".NET Blog", });
Das Überprüfen der Debugansicht der Änderungsverfolgung nach diesem Aufruf zeigt, dass der Kontext diese Entität im Modified
-Zustand nachverfolgt:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: []
Genau wie bei Add
und Attach
, markiert Update
tatsächlich ein ganzes Diagramm verwandter Entitäten als Modified
. So fügen Sie z. B. einen vorhandenen Blog und zugeordnete vorhandene Beiträge wie Modified
hinzu:
context.Update(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
}
}
});
Der Kontext verfolgt nun alle diese Entitäten wie Modified
:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
Id: 1 PK
BlogId: 1 FK Modified Originally <null>
Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
Title: 'Announcing the Release of EF Core 5.0' Modified
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK Modified Originally <null>
Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
Title: 'Announcing F# 5' Modified
Blog: {Id: 1}
Das Aufrufen von SaveChanges an diesem Punkt führt dazu, dass Aktualisierungen für alle diese Entitäten an die Datenbank gesendet werden. Ein Beispiel sehen Sie bei der Verwendung von SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
Generierte Schlüsselwerte
Wie bei Attach
haben generierte Schlüsselwerte den gleichen Hauptvorteil für Update
: Ein nicht festgelegter Schlüsselwert gibt an, dass die Entität neu ist und noch nicht in die Datenbank eingefügt wurde. Wie bei Attach
ermöglicht dies dem DbContext, neue Entitäten automatisch zu erkennen und sie in den Added
-Zustand zu versetzen. Erwägen Sie z. B. das Aufrufen von Update
mit diesem Diagramm eines Blogs und Beiträgen:
context.Update(
new Blog
{
Id = 1,
Name = ".NET Blog",
Posts =
{
new Post
{
Id = 1,
Title = "Announcing the Release of EF Core 5.0",
Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
},
new Post
{
Id = 2,
Title = "Announcing F# 5",
Content = "F# 5 is the latest version of F#, the functional programming language..."
},
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
},
}
});
Wie im Beispiel Attach
wird der Beitrag ohne Schlüsselwert als neu erkannt und in den Added
-Zustand versetzt. Die anderen Entitäten werden als Modified
markiert:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog' Modified
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
Id: -2147482633 PK Temporary
BlogId: 1 FK
Content: '.NET 5.0 includes many enhancements, including single file a...'
Title: 'Announcing .NET 5.0'
Blog: {Id: 1}
Post {Id: 1} Modified
Id: 1 PK
BlogId: 1 FK Modified Originally <null>
Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
Title: 'Announcing the Release of EF Core 5.0' Modified
Blog: {Id: 1}
Post {Id: 2} Modified
Id: 2 PK
BlogId: 1 FK Modified Originally <null>
Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
Title: 'Announcing F# 5' Modified
Blog: {Id: 1}
Das Aufrufen von SaveChanges
an diesem Punkt bewirkt, dass Aktualisierungen für alle vorhandenen Entitäten an die Datenbank gesendet werden, während die neue Entität eingefügt wird. Ein Beispiel sehen Sie bei der Verwendung von SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();
Dies ist eine sehr einfache Möglichkeit, Aktualisierungen und Einfügungen aus einem getrennten Diagramm zu generieren. Es führt jedoch dazu, dass Aktualisierungen oder Einfügungen für jede Eigenschaft jeder nachverfolgten Entität an die Datenbank gesendet werden, auch wenn einige Eigenschaftswerte möglicherweise nicht geändert wurden. Fürchten Sie sich nicht. Für viele Anwendungen mit kleinen Diagrammen kann dies eine einfache und pragmatische Möglichkeit zum Generieren von Updates sein. Das heißt, andere komplexere Muster können manchmal zu effizienteren Updates führen, wie in der Identitätsauflösung in EF Core beschrieben.
Löschen vorhandener Entitäten
Damit eine Entität von SaveChanges gelöscht werden kann, muss sie im Deleted
-Zustand nachverfolgt werden. Entitäten werden in der Regel in den Deleted
-Zustand versetzt, indem sie von DbContext.Remove, DbContext.RemoveRange oder der entsprechenden Methoden von DbSet<TEntity> aufgerufen werden. So markieren Sie z. B. einen vorhandenen Beitrag als Deleted
:
context.Remove(
new Post { Id = 2 });
Das Überprüfen der Debugansicht der Änderungsverfolgung nach diesem Aufruf zeigt, dass der Kontext die Entität im Deleted
-Zustand nachverfolgt:
Post {Id: 2} Deleted
Id: 2 PK
BlogId: <null> FK
Content: <null>
Title: <null>
Blog: <null>
Diese Entität wird gelöscht, wenn SaveChanges aufgerufen wird. Ein Beispiel sehen Sie bei der Verwendung von SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Nach Abschluss von SaveChanges wird die gelöschte Entität vom DbContext getrennt, da sie nicht mehr in der Datenbank vorhanden ist. Die Debugansicht ist daher leer, weil keine Entitäten nachverfolgt werden.
Löschen abhängiger/untergeordneter Entitäten
Das Löschen abhängiger/untergeordneter Entitäten aus einem Diagramm ist einfacher als das Löschen von Prinzipal-/übergeordneter Entitäten. Weitere Informationen finden Sie im nächsten Abschnitt und in Ändern von Fremdschlüsseln und Navigationen.
Es ist ungewöhnlich, Remove
für eine Entität aufzurufen, die mit new
erstellt wurde. Im Gegensatz zu Add
, Attach
und Update
ist es ungewöhnlich, Remove
für eine Entität aufzurufen, die noch nicht im Unchanged
- oder Modified
-Zustand nachverfolgt wird. Stattdessen ist es typisch, eine einzelne Entität oder ein einzelnes Diagramm verwandter Entitäten nachzuverfolgen, und dann Remove
für die Entitäten aufzurufen, die gelöscht werden sollen. Dieses Diagramm der nachverfolgten Entitäten wird in der Regel von einem der Folgenden erstellt:
- Ausführen einer Abfrage für die Entitäten
- Verwenden der
Attach
- oderUpdate
-Methoden für ein Diagramm von getrennten Entitäten, wie in den vorherigen Abschnitten beschrieben.
Beispielsweise ist es wahrscheinlicher, dass der Code im vorherigen Abschnitt einen Beitrag von einem Client abruft und dann etwas Ähnliches wie Folgendes ausführt:
context.Attach(post);
context.Remove(post);
Dies verhält sich genau so wie im vorherigen Beispiel, da das Aufrufen von Remove
auf einer nicht nachverfolgten Entität bewirkt, dass sie zuerst angefügt und dann als Deleted
gekennzeichnet wird.
In realistischeren Beispielen wird zunächst ein Diagramm mit Entitäten angefügt, und dann werden einige dieser Entitäten als gelöscht markiert. Beispiel:
// Attach a blog and associated posts
context.Attach(blog);
// Mark one post as Deleted
context.Remove(blog.Posts[1]);
Alle Entitäten werden als Unchanged
gekennzeichnet, mit Ausnahme der Entitäten, für die Remove
aufgerufen wurde:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Diese Entität wird gelöscht, wenn SaveChanges aufgerufen wird. Ein Beispiel sehen Sie bei der Verwendung von SQLite:
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
Nach Abschluss von SaveChanges wird die gelöschte Entität vom DbContext getrennt, da sie nicht mehr in der Datenbank vorhanden ist. Andere Entitäten verbleiben im Unchanged
-Status:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}]
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Löschen von Prinzipal-/übergeordneten Entitäten
Jede Beziehung, die zwei Entitätstypen verbindet, weist ein Prinzipal- oder übergeordnetes Ende und ein abhängiges oder untergeordnetes Ende auf. Die abhängige/untergeordnete Entität ist die Entität mit der Fremdschlüsseleigenschaft. In einer 1:n-Beziehung befindet sich der Prinzipal/das übergeordnete Element auf der 1-Seite, und das abhängige/untergeordnete Element befindet sich auf der n-Seite. Weitere Informationen finden Sie unter Beziehungen.
In den vorherigen Beispielen haben wir einen Beitrag gelöscht, der eine abhängige/untergeordnete Entität in der 1:n-Beziehung von Blogbeiträgen war. Dies ist relativ einfach, da das Entfernen einer abhängigen/untergeordneten Entität keine Auswirkungen auf andere Entitäten hat. Andererseits wirkt sich das Löschen einer Prinzipal-/übergeordneten Entität auch auf abhängige/untergeordnete Entitäten aus. Wenn nicht würde ein Fremdschlüsselwert, der auf einen nicht mehr vorhandenen Primärschlüsselwert verweist, beibehalten. Das wäre ein ungültiger Modellstatus und führt in den meisten Datenbanken zu einem referentiellen Einschränkungsfehler.
Dieser ungültige Modellzustand kann auf zwei Arten behandelt werden:
- Festlegen der FK (Fremdschlüssel)-Werte auf NULL. Dies gibt an, dass die abhängigen/untergeordneten Elemente nicht mehr mit einem Prinzipal/übergeordneten Element verknüpft sind. Das ist die Standardeinstellung für optionale Beziehungen, bei denen der Fremdschlüssel nullfähig sein muss. Das Festlegen des FK auf NULL ist für erforderliche Beziehungen, bei denen der Fremdschlüssel in der Regel nicht nullfähig ist, ungültig.
- Löschen der abhängigen/untergeordneten Elemente. Dies ist die Standardeinstellung für erforderliche Beziehungen und gilt auch für optionale Beziehungen.
Ausführliche Informationen zur Änderungsnachverfolgung und zu Beziehungen finden Sie unter Ändern von Fremdschlüsseln und Navigationen.
Optionale Beziehungen
Die Fremdschlüsseleigenschaft Post.BlogId
ist im verwendeten Modell nullfähig. Dies bedeutet, dass die Beziehung optional ist und daher das Standardverhalten von EF Core darin besteht, die BlogId
-Fremdschlüsseleigenschaften auf NULL festzulegen, wenn der Blog gelöscht wird. Beispiel:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
Überprüfen der Debugansicht der Änderungsverfolgung nach dem Aufruf von Remove
, dass der Blog jetzt, wie erwartet, als Deleted
markiert ist:
Blog {Id: 1} Deleted
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
Id: 1 PK
BlogId: <null> FK Modified Originally 1
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: <null>
Post {Id: 2} Modified
Id: 2 PK
BlogId: <null> FK Modified Originally 1
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Interessanter ist, dass alle zugehörigen Beiträge jetzt als Modified
gekennzeichnet sind. Dies liegt daran, dass die Fremdschlüsseleigenschaft in jeder Entität auf NULL festgelegt wurde. Durch das Aufrufen von SaveChanges wird der Fremdschlüsselwert für jeden Beitrag in der Datenbank auf NULL aktualisiert, bevor der Blog gelöscht wird:
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();
Nach Abschluss von SaveChanges wird die gelöschte Entität vom DbContext getrennt, da sie nicht mehr in der Datenbank vorhanden ist. Andere Entitäten werden jetzt als Unchanged
mit NULL-Fremdschlüsselwerten markiert, die dem Status der Datenbank entsprechen:
Post {Id: 1} Unchanged
Id: 1 PK
BlogId: <null> FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: <null>
Post {Id: 2} Unchanged
Id: 2 PK
BlogId: <null> FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: <null>
Erforderliche Beziehungen
Wenn die Post.BlogId
-Fremdschlüsseleigenschaft nicht nullfähig ist, wird die Beziehung zwischen Blogs und Beiträgen erforderlich. In diesem Fall löscht EF Core standardmäßig abhängige/untergeordnete Entitäten, wenn der Prinzipal/das übergeordnete Element gelöscht wird. Beispiel: Löschen eines Blogs mit verwandten Beiträgen wie im vorherigen Beispiel:
// Attach a blog and associated posts
context.Attach(blog);
// Mark the blog as deleted
context.Remove(blog);
Das Überprüfen der Debugansicht der Änderungsüberwachung nach dem Aufruf von Remove
zeigt, dass der Blog wie erwartet erneut als Deleted
markiert ist:
Blog {Id: 1} Deleted
Id: 1 PK
Name: '.NET Blog'
Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
Id: 1 PK
BlogId: 1 FK
Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
Title: 'Announcing the Release of EF Core 5.0'
Blog: {Id: 1}
Post {Id: 2} Deleted
Id: 2 PK
BlogId: 1 FK
Content: 'F# 5 is the latest version of F#, the functional programming...'
Title: 'Announcing F# 5'
Blog: {Id: 1}
Interessanter ist in diesem Fall, dass alle zugehörigen Beiträge ebenfalls als Deleted
gekennzeichnet wurden. Durch Aufrufen von SaveChanges werden der Blog und alle zugehörigen Beiträge aus der Datenbank gelöscht:
-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();
-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;
Nach Abschluss von SaveChanges werden alle gelöschten Entitäten vom DbContext getrennt, da sie nicht mehr in der Datenbank vorhanden sind. Die Ausgabe aus der Debugansicht ist daher leer.
Hinweis
In diesem Dokument wird die Arbeit mit Beziehungen in EF Core nur oberflächlich behandelt. Weitere Informationen zum Modellieren von Beziehungen sowie zum Ändern von Fremdschlüsseln und Navigationen finden Sie unter Beziehungen, um weitere Informationen zum Aktualisieren/Löschen abhängiger/untergeordneter Entitäten beim Aufrufen von SaveChanges zu erhalten.
Benutzerdefinierte Nachverfolgung mit TrackGraph
ChangeTracker.TrackGraph funktioniert wie Add
, Attach
und Update
mit der Ausnahme, dass sie einen Rückruf für jede Entitätsinstanz generiert, bevor sie nachverfolgt wird. Auf diese Weise kann beim Bestimmen, wie einzelne Entitäten in einem Diagramm nachverfolgt werden, benutzerdefinierte Logik verwendet werden.
Betrachten Sie beispielsweise die Regel, die EF Core beim Nachverfolgen von Entitäten mit generierten Schlüsselwerten verwendet: Wenn der Schlüsselwert null ist, ist die Entität neu und sollte eingefügt werden. Lassen Sie uns diese Regel erweitern, indem wir sagen, dass wenn der Schlüsselwert negativ ist, die Entität gelöscht werden sollte. Auf diese Weise können wir die Primärschlüsselwerte in Entitäten eines getrennten Diagramms umwandeln, um gelöschte Entitäten zu markieren:
blog.Posts.Add(
new Post
{
Title = "Announcing .NET 5.0",
Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
}
);
var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;
Dieses getrennte Diagramm kann dann mithilfe von TrackGraph nachverfolgt werden:
public static void UpdateBlog(Blog blog)
{
using var context = new BlogsContext();
context.ChangeTracker.TrackGraph(
blog, node =>
{
var propertyEntry = node.Entry.Property("Id");
var keyValue = (int)propertyEntry.CurrentValue;
if (keyValue == 0)
{
node.Entry.State = EntityState.Added;
}
else if (keyValue < 0)
{
propertyEntry.CurrentValue = -keyValue;
node.Entry.State = EntityState.Deleted;
}
else
{
node.Entry.State = EntityState.Modified;
}
Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
});
context.SaveChanges();
}
Für jede Entität im Diagramm überprüft der obige Code den Primärschlüsselwert, bevor die Entität nachverfolgt wird. Bei nicht festgelegten (Null)-Schlüsselwerten führt der Code aus, was EF Core normalerweise tun würde. Das bedeutet, wenn der Schlüssel nicht festgelegt ist, wird die Entität als Added
gekennzeichnet. Wenn der Schlüssel festgelegt und der Wert nicht negativ ist, wird die Entität als Modified
gekennzeichnet. Wenn jedoch ein negativer Schlüsselwert gefunden wird, wird der tatsächliche, nicht negative Wert wiederhergestellt, und die Entität wird als Deleted
nachverfolgt.
Die Ausgabe aus der Ausführung dieses Codes lautet:
Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added
Hinweis
Aus Gründen der Einfachheit geht dieser Code davon aus, dass jede Entität über eine ganzzahlige Primärschlüsseleigenschaft namens Id
verfügt. Dies könnte in eine abstrakte Basisklasse oder Schnittstelle codiert werden. Alternativ können die Primärschlüsseleigenschaft oder -eigenschaften aus den IEntityType-Metadaten abgerufen werden, sodass dieser Code mit jedem Entitätstyp funktionieren würde.
TrackGraph verfügt über zwei Überladungen. In der oben verwendeten einfachen Überladung bestimmt EF Core, wann das Durchlaufen des Diagramms beendet werden soll. Insbesondere wird der Besuch neuer verwandter Entitäten von einer bestimmten Entität beendet, wenn diese Entität entweder bereits nachverfolgt wurde oder wenn der Rückruf nicht mit der Nachverfolgung der Entität beginnt.
Die erweiterte Überladung ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>) weist einen Rückruf auf, der einen Bool-Wert zurückgibt. Wenn der Rückruf falsch zurückgibt, wird das Durchlaufen des Diagramms beendet, andernfalls wird es fortgesetzt. Es muss darauf geachtet werden, endlose Schleifen bei Verwendung dieser Überladung zu vermeiden.
Die erweiterte Überladung ermöglicht außerdem die Angabe des Zustands an TrackGraph, und dieser Zustand wird dann an jeden Rückruf übergeben.