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:

  1. Erstellen neuer Entitäten, die in die Datenbank eingefügt werden.
  2. 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:

  1. Ausführen einer Abfrage für die Entitäten
  2. Verwenden der Attach- oder Update-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:

  1. 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.
  2. 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 Modifiedgekennzeichnet 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.