Ś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ć:

  1. Utworzenie wystąpienia obiektu DbContext
  2. Śledzenie jednostek
  3. Wprowadzanie zmian w jednostkach
  4. Wywołanie metody SaveChanges w celu zaktualizowania bazy danych
  5. 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 Unchangednie 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 Aktualizuj
Deleted Tak Tak - Usuń

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 śledzenia zmian to doskonały sposób wizualizowania śledzonych jednostek i ich stanów. 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 stanie Modified.
  • 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 stanie Modified.
  • 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. 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.