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:
- Vytvoření instance DbContext
- Sledování některých entit
- Provedení některých změn entit
- Volání SaveChanges pro aktualizaci databáze
- 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 |
No | - | - | - |
Added |
Ano | No | - | Vložit |
Unchanged |
Ano | Ano | Ne | - |
Modified |
Ano | Ano | Yes | Aktualizovat |
Deleted |
Ano | Yes | - | 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 = await context.Blogs.Include(e => e.Posts).FirstAsync(e => e.Name == ".NET Blog");
blog.Name = ".NET Blog (Updated!)";
await foreach (var post in blog.Posts.AsQueryable().Where(e => !e.Title.Contains("5.0")).AsAsyncEnumerable())
{
post.Title = post.Title.Replace("5", "5.0");
}
await context.SaveChangesAsync();
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í 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 stavuModified
. - 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 stavuModified
. - 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 = await context.Blogs.Include(e => e.Posts).FirstAsync(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);
await context.SaveChangesAsync();
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.