Sledování změn v EF Core

Každá instance DbContext sleduje změny provedené u entit. Tyto sledované entity následně řídí změny v databázi při volání SaveChanges.

Tento dokument představuje přehled sledování změn Entity Framework Core (EF Core) a jeho vztah k dotazům a aktualizacím.

Tip

Celý kód v tomto dokumentu můžete spustit a ladit tak, že si stáhnete ukázkový kód z GitHubu.

Tip

Pro zjednodušení se v tomto dokumentu používají a odkazují synchronní metody, jako jsou SaveChanges, a nikoli jejich asynchronní ekvivalenty, jako jsou SaveChangesAsync. Volání a čekání na asynchronní metodu lze nahradit, pokud není uvedeno jinak.

Jak sledovat entity

Instance entit se sledují, když jsou:

  • Vrácené z dotazu spuštěného v databázi
  • Explicitně připojené k DbContext pomocí Add, Attach, Update nebo podobné metody
  • Detekované jako nové entity propojené s existujícími sledovanými entitami

Instance entit se už nesledují, když:

  • DbContext je odstraněn.
  • Sledování změn je vymazáno.
  • Entity jsou explicitně odpojené.

DbContext je navržený tak, aby představoval krátkodobou jednotku práce, jak je popsáno v tématu o inicializaci a konfiguraci DbContext. To znamená, že rozložení DbContext je normální způsob, zastavit sledování entit. Jinými slovy, doba života DbContext by měla být:

  1. Vytvoření instance DbContext
  2. Sledování některých entit
  3. Provedení některých změn entit
  4. Volání SaveChanges pro aktualizaci databáze
  5. Uvolnění instance DbContext

Tip

Při tomto přístupu není nutné vymazat sledování změn ani explicitně odpojovat instance entit. Pokud však potřebujete oddělit entity, je volání ChangeTracker.Clear efektivnější než oddělování entit po jedné.

Stavy entit

Každá entita je přidružená k danému EntityState:

  • DbContext nesleduje entity Detached.
  • Entity Added jsou nové a ještě nebyly vloženy do databáze. To znamená, že budou vloženy při volání SaveChanges.
  • Entity Unchanged se nezměnily od doby, kdy byly dotazovány z databáze. Všechny entity vrácené z dotazů jsou zpočátku v tomto stavu.
  • Entity Modified se změnily od doby, kdy byly dotazovány z databáze. To znamená, že se po volání SaveChanges aktualizují.
  • Entity Deleted existují v databázi, ale jsou označeny k odstranění při volání SaveChanges.

EF Core sleduje změny na úrovni vlastností. Pokud je například změněna pouze jedna hodnota vlastnosti, aktualizace databáze změní pouze tuto hodnotu. Vlastnosti však mohou být označeny jako změněné pouze tehdy, když je samotná entita ve stavu Modified. (Nebo – z jiného pohledu – stav Modified znamená, že alespoň jedna hodnota vlastnosti byla označena jako změněná.)

Následující tabulka shrnuje jednotlivé stavy:

Stav entity Sledováno pomocí DbContext Existuje v databázi Vlastnosti změněny Akce při SaveChanges
Detached Číslo - - -
Added Ano Ne - Vložit
Unchanged Ano Ano No -
Modified Ano Ano Ano Aktualizovat
Deleted Ano Ano - Odstranění

Poznámka

V tomto textu jsou pro přehlednost použity termíny relační databáze. Databáze NoSQL obvykle podporují podobné operace, ale případně s jinými názvy. Další informace najdete v dokumentaci poskytovatele databáze.

Sledování z dotazů

Sledování změn EF Core funguje nejlépe, když se stejná instance DbContext používá jak k dotazování na entity, tak k jejich aktualizaci voláním SaveChanges. Je to proto, že EF Core automaticky sleduje stav dotazovaných entit a poté zjišťuje všechny změny provedené v těchto entitách při volání SaveChanges.

Tento přístup má několik výhod oproti explicitnímu sledování instancí entit:

  • Je jednoduchý. Stavy entit pouze zřídka potřebují být manipulovány explicitně – EF Core se postará o změny stavu.
  • Aktualizace jsou omezené pouze na hodnoty, které se skutečně změnily.
  • Hodnoty stínových vlastností se zachovají a použijí podle potřeby. To platí zejména v případě, že jsou cizí klíče uložené ve stínovém stavu.
  • Původní hodnoty vlastností se zachovají automaticky a používají se k efektivním aktualizacím.

Jednoduchý dotaz a aktualizace

Představte si například jednoduchý model blogů/příspěvků:

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

Tento model můžeme použít k vyhledávání blogů a příspěvků a následně provést některé aktualizace databáze:

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

blog.Name = ".NET Blog (Updated!)";

foreach (var post in blog.Posts.Where(e => !e.Title.Contains("5.0")))
{
    post.Title = post.Title.Replace("5", "5.0");
}

context.SaveChanges();

Volání SaveChanges vede k následujícím aktualizacím databáze pomocí SQLite jako ukázkové databáze:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0='Announcing F# 5.0' (Size = 17)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "Title" = @p0
WHERE "Id" = @p1;
SELECT changes();

Zobrazení ladění modulu sledování změn je skvělý způsob, jak vizualizovat, které entity se sledují a jaké jsou jejich stavy. Například vložením následujícího kódu do výše uvedené ukázky před voláním SaveChanges:

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Generuje následující výstup:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}]
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} Modified
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
  Blog: {Id: 1}

Všimněte si následujících podrobností:

  • Vlastnost Blog.Name je označena jako změněná (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog') a výsledkem je blog ve stavu Modified.
  • Vlastnost Post.Title příspěvku 2 je označena jako změněná (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5') a výsledkem je tento příspěvek ve stavu Modified.
  • Ostatní hodnoty vlastností příspěvku 2 se nezměnily, a proto nejsou označené jako změněné. Proto nejsou tyto hodnoty zahrnuty do aktualizace databáze.
  • Druhý příspěvek nebyl nijak změněn. Proto je stále ve stavu Unchanged a není zahrnut do aktualizace databáze.

Dotaz, pak vložení, aktualizace a odstranění

Aktualizace jako v předchozím příkladu je možné kombinovat s vloženími a odstraněními ve stejné jednotce práce. Příklad:

using var context = new BlogsContext();

var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");

// Modify property values
blog.Name = ".NET Blog (Updated!)";

// Insert a new Post
blog.Posts.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?", Content = ".NET 5.0 was released recently and has come with many..."
    });

// Mark an existing Post as Deleted
var postToDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
context.Remove(postToDelete);

context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

V tomto příkladu:

  • Blog a související příspěvky se dotazují z databáze a sledují.
  • Vlastnost Blog.Name je změněna.
  • Do kolekce existujících příspěvků pro blog se přidá nový příspěvek.
  • Existující příspěvek je označen k odstranění voláním DbContext.Remove

Když se znovu podíváme na zobrazení ladění modulu sledování změn před voláním SaveChanges, je vidět, jak EF Core tyto změny sleduje:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: 3}, {Id: -2147482638}]
Post {Id: -2147482638} Added
  Id: -2147482638 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 was released recently and has come with many...'
  Title: 'What's next for System.Text.Json?'
  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} 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}

Všimněte si, že:

  • Blog je označen jako Modified. Tím se vygeneruje aktualizace databáze.
  • Příspěvek 2 je označen jako Deleted. Tím se vygeneruje odstranění databáze.
  • Nový příspěvek s dočasným ID je přidružený k blogu 1 a je označen jako Added. Tím se vygeneruje vložení do databáze.

Výsledkem jsou následující databázové příkazy (pomocí SQLite), když se volá SaveChanges:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog (Updated!)' (Size = 20)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
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=[@p0='1' (DbType = String), @p1='.NET 5.0 was released recently and has come with many...' (Size = 56), @p2='What's next for System.Text.Json?' (Size = 33)], 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();

Další informace o vkládání a odstraňování entit najdete v tématu o explicitním sledování entit . Další informace o tom, jak EF Core automaticky rozpozná změny, jako je tato, najdete v tématu o detekci změn a oznámeních .

Tip

Voláním ChangeTracker.HasChanges() zjistěte, jestli byly provedeny nějaké změny, které způsobí, že funkce SaveChanges provede aktualizace databáze. Pokud HasChanges vrátí hodnotu false, SaveChanges bude no-op.