Śledzenie zmian w rozwiązaniu EF Core
Każde wystąpienie obiektu DbContext śledzi zmiany wprowadzane w jednostkach. Te śledzone jednostki sterują z kolei zmianami w bazie danych przy wywoływaniu metody SaveChanges.
W tym dokumencie omówiono śledzenie zmian w rozwiązaniu Entity Framework Core (EF Core) oraz jego powiązania z zapytaniami i aktualizacjami.
Napiwek
Możesz uruchomić i debugować cały kod podany w tym dokumencie, pobierając przykładowy kod z serwisu GitHub.
Napiwek
Dla uproszczenia w tym dokumencie są używane metody synchroniczne, takie jak SaveChanges, a nie ich odpowiedniki asynchroniczne, takie jak SaveChangesAsync. Alternatywnie można wywoływać metodę asynchroniczną i oczekiwać na nią, chyba że określono inaczej.
Jak śledzić jednostki
Wystąpienia jednostek są śledzone, gdy są one:
- Zwracane z zapytania uruchomionego względem bazy danych
- Jawnie dołączane do obiektu DbContext za pomocą metody
Add
,Attach
,Update
lub podobnej - Wykrywane jako nowe jednostki połączone z istniejącymi śledzonymi jednostkami
Wystąpienia jednostek przestają być śledzone, gdy:
- Obiekt DbContext jest usuwany
- Monitor zmian jest czyszczone
- Jednostki są jawnie odłączane
Obiekt DbContext jest przeznaczony do reprezentowania krótkotrwałej jednostki pracy, zgodnie z opisem w temacie Inicjalizacja i konfiguracja obiektu DbContext. Oznacza to, że usuwanie obiektu DbContext jest normalnym sposobem zatrzymywania śledzenia jednostek. Innymi słowy, okres istnienia obiektu DbContext powinien obejmować:
- Utworzenie wystąpienia obiektu DbContext
- Śledzenie jednostek
- Wprowadzanie zmian w jednostkach
- Wywołanie metody SaveChanges w celu zaktualizowania bazy danych
- Usunięcie wystąpienia obiektu DbContext
Napiwek
Nie jest konieczne czyszczenie monitora zmian ani jawne odłączanie wystąpień jednostek w przypadku stosowania tego podejścia. Jeśli jednak konieczne jest odłączanie jednostek, wywoływanie metody ChangeTracker.Clear jest efektywniejsze niż odłączanie jednostek pojedynczo.
Stany jednostek
Każda jednostka jest skojarzona z określonym obiektem EntityState:
- Jednostki
Detached
nie są śledzone przez obiekt DbContext. - Jednostki
Added
są nowe i nie zostały jeszcze wstawione do bazy danych. Oznacza to, że zostaną wstawione przy wywołaniu metody SaveChanges. - Jednostki
Unchanged
nie zostały zmienione od czasu zwrócenia ich przez zapytanie z bazy danych. Wszystkie jednostki zwracane z zapytań są początkowo w tym stanie. - Jednostki
Modified
zostały zmienione od czasu zwrócenia ich przez zapytanie z bazy danych. Oznacza to, że zostaną zaktualizowane przy wywołaniu metody SaveChanges. - Jednostki
Deleted
istnieją w bazie danych, ale są oznaczone do usunięcia przy wywołaniu metody SaveChanges.
Rozwiązanie EF Core śledzi zmiany na poziomie właściwości. Jeśli na przykład zostanie zmodyfikowana tylko jedna wartość właściwości, aktualizacja bazy danych zmieni tylko tę wartość. Jednak właściwości można oznaczać jako zmodyfikowane tylko wtedy, gdy jednostka jest w stanie Modified. (Inaczej rzecz biorąc, stan Modified oznacza, że co najmniej jedna wartość właściwości została oznaczona jako zmodyfikowana).
Poniższa tabela zawiera podsumowanie różnych stanów:
Stan encji | Śledzona przez obiekt DbContext | Istnieje w bazie danych | Właściwości zostały zmodyfikowane | Akcja w metodzie SaveChanges |
---|---|---|---|---|
Detached |
Nie. | - | - | - |
Added |
Tak | Nie. | - | Insert |
Unchanged |
Tak | Tak | Nie | - |
Modified |
Tak | Tak | Tak | Zaktualizuj |
Deleted |
Tak | Tak | - | Delete |
Uwaga
Dla jasności w tym tekście są używane terminy dotyczące relacyjnych baz danych. Bazy danych NoSQL zwykle obsługują podobne operacje, ale potencjalnie z innymi nazwami. Aby uzyskać więcej informacji, zapoznaj się z dokumentacją dostawcy baz danych.
Śledzenie na podstawie zapytań
Śledzenie zmian w rozwiązaniu EF Core działa najlepiej, gdy to samo wystąpienie obiektu DbContext jest używane do uruchamiania zapytań dotyczących jednostek i aktualizowania ich przez wywołanie metody SaveChanges. Jest tak dlatego, że rozwiązanie EF Core automatycznie śledzi stan jednostek określonych w zapytaniu, a następnie przy wywołaniu metody SaveChanges wykrywa wszelkie zmiany wprowadzone w tych jednostkach.
Takie podejście ma kilka zalet w porównaniu z jawnym śledzeniem wystąpień jednostek:
- Jest to proste. Stan jednostek rzadko musi być modyfikowany jawnie — zmiany stanu są obsługiwane przez rozwiązanie EF Core.
- Aktualizacje są ograniczone tylko do tych wartości, które rzeczywiście uległy zmianie.
- Wartości właściwości towarzyszących są zachowywane i używane zgodnie z potrzebami. Jest to szczególnie istotne, gdy w stanie towarzyszącym są przechowywane klucze obce.
- Oryginalne wartości właściwości są zachowywane automatycznie i używane do efektywnych aktualizacji.
Proste zapytanie i aktualizacja
Rozważmy na przykład prosty model blogów/wpisów:
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; }
}
Ten model umożliwia uruchamianie zapytań dotyczących blogów i wpisów, a następnie wprowadzanie aktualizacji bazy danych:
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();
Wywołanie metody SaveChanges powoduje wprowadzenie następujących aktualizacji bazy danych (jako przykład jest używana baza danych SQLite):
-- 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();
Widok debugowania monitora zmian to doskonały sposób wizualizacji, które jednostki są śledzone i jakie są ich stany. Na przykład wstawienie poniższego kodu w powyższym przykładzie przed wywołaniem metody SaveChanges:
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Generuje następujące dane wyjściowe:
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}
W szczególności zwróć uwagę na następujące kwestie:
- Właściwość
Blog.Name
jest oznaczana jako zmodyfikowana (Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
), co powoduje, że blog jest w stanieModified
. - Właściwość
Post.Title
wpisu 2 jest oznaczana jako zmodyfikowana (Title: 'Announcing F# 5.0' Modified Originally 'Announcing F# 5'
),co powoduje, że ten wpis jest w stanieModified
. - Inne wartości właściwości wpisu 2 nie zostały zmienione, więc nie są oznaczone jako zmodyfikowane. Dlatego te wartości nie są uwzględniane w aktualizacji bazy danych.
- Drugi z wpisów nie został zmodyfikowany w żaden sposób. Dlatego jest on nadal w stanie
Unchanged
i nie jest uwzględniany w aktualizacji bazy danych.
Uruchamianie zapytania oraz wstawianie, aktualizowanie i usuwanie
Aktualizacje podobne do tych w powyższym przykładzie można łączyć, wykonując operacje wstawiania i usuwania w tej samej jednostce pracy. Na przykład:
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();
W tym przykładzie:
- Blog i powiązane wpisy są pobierane za pomocą zapytania z bazy danych i śledzone
- Zmieniana jest właściwość
Blog.Name
- Dodawany jest nowy wpis do kolekcji istniejących wpisów w blogu
- Istniejący wpis jest oznaczany do usunięcia przez wywołanie metody DbContext.Remove
Ponownie sprawdzając widok debugowania monitora zmian przed wywołaniem funkcji SaveChanges, można zobaczyć, jak rozwiązanie EF Core śledzi te zmiany:
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}
Zwróć uwagę, że:
- Blog jest oznaczony jako
Modified
. Spowoduje to wygenerowanie aktualizacji w bazie danych. - Wpis 2 jest oznaczony jako
Deleted
. Spowoduje to wygenerowanie usunięcia w bazie danych. - Nowy wpis z tymczasowym identyfikatorem jest skojarzony z blogiem 1 i jest oznaczony jako
Added
. Spowoduje to wygenerowanie wstawienia w bazie danych.
W rezultacie zostaną uruchomione następujące polecenia bazy danych (przy użyciu bazy danych SQLite) przy wywołaniu metody 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();
Aby uzyskać więcej informacji o wstawianiu i usuwaniu jednostek, zobacz Jawne śledzenie jednostek. Zobacz Wykrywanie zmian i powiadomienia, aby uzyskać więcej informacji o automatycznym wykrywaniu zmian przez rozwiązanie EF Core.
Napiwek
Wywołuj metodę ChangeTracker.HasChanges(), aby ustalić, czy wprowadzono zmiany, które spowodują wprowadzenie aktualizacji bazy danych przez metodę SaveChanges. Jeśli metoda HasChanges zwraca wartość false, oznacza to, że metoda SaveChanges nie wykona żadnych operacji.