Wykrywanie zmian i powiadomienia
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. Opisano to w temacie Change Tracking in EF Core (Śledzenie zmian w programie EF Core), a w tym dokumencie założono, że stany jednostek i podstawy śledzenia zmian platformy Entity Framework Core (EF Core) są zrozumiałe.
Śledzenie zmian właściwości i relacji wymaga, aby obiekt DbContext mógł wykryć te zmiany. W tym dokumencie opisano sposób wykrywania, a także sposób używania powiadomień dotyczących właściwości lub serwerów proxy śledzenia zmian w celu wymuszenia natychmiastowego wykrywania zmian.
Napiwek
Możesz uruchomić i debugować cały kod podany w tym dokumencie, pobierając przykładowy kod z serwisu GitHub.
Śledzenie zmian migawek
Domyślnie program EF Core tworzy migawkę wartości właściwości każdej jednostki, gdy jest ona najpierw śledzona przez wystąpienie DbContext. Wartości przechowywane w tej migawce są następnie porównywane z bieżącymi wartościami jednostki w celu określenia, które wartości właściwości uległy zmianie.
To wykrywanie zmian występuje po wywołaniu metody SaveChanges w celu upewnienia się, że wszystkie zmienione wartości zostaną wykryte przed wysłaniem aktualizacji do bazy danych. Jednak wykrywanie zmian odbywa się również w innym czasie, aby upewnić się, że aplikacja współpracuje z aktualnymi informacjami o śledzeniu. Wykrywanie zmian można wymusić w dowolnym momencie przez wywołanie metody ChangeTracker.DetectChanges().
Gdy jest wymagane wykrywanie zmian
Wykrywanie zmian jest wymagane, gdy właściwość lub nawigacja została zmieniona bez używania programu EF Core do wprowadzenia tej zmiany. Rozważ na przykład ładowanie blogów i wpisów, a następnie wprowadzanie zmian w następujących jednostkach:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
// Change a property value
blog.Name = ".NET Blog (Updated!)";
// Add a new entity to a navigation
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..."
});
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
context.ChangeTracker.DetectChanges();
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Patrząc na widok debugowania śledzenia zmian przed wywołaniem ChangeTracker.DetectChanges() , widać, że wprowadzone zmiany nie zostały wykryte, dlatego nie są odzwierciedlane w stanach jednostki i zmodyfikowanych danych właściwości:
Blog {Id: 1} Unchanged
Id: 1 PK
Name: '.NET Blog (Updated!)' Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, <not found>]
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}
W szczególności stan wpisu w blogu to , Unchanged
a nowy wpis nie jest wyświetlany jako śledzona jednostka. (Informacje o właściwościach będą raportować ich nowe wartości, mimo że te zmiany nie zostały jeszcze wykryte przez program EF Core. Dzieje się tak, ponieważ widok debugowania odczytuje bieżące wartości bezpośrednio z wystąpienia jednostki).
Porównaj to z widokiem debugowania po wywołaniu funkcji DetectChanges:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified Originally '.NET Blog'
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
Id: -2147482643 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} 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}
Teraz blog jest poprawnie oznaczony jako Modified
i nowy wpis został wykryty i jest śledzony jako Added
.
Na początku tej sekcji stwierdziliśmy, że wykrywanie zmian jest wymagane, gdy nie jest używane program EF Core do wprowadzenia zmiany. Dzieje się tak w powyższym kodzie. Oznacza to, że zmiany właściwości i nawigacji są wprowadzane bezpośrednio w wystąpieniach jednostki, a nie przy użyciu żadnych metod platformy EF Core.
Porównaj to z następującym kodem, który modyfikuje jednostki w ten sam sposób, ale tym razem przy użyciu metod EF Core:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
// Change a property value
context.Entry(blog).Property(e => e.Name).CurrentValue = ".NET Blog (Updated!)";
// Add a new entity to the DbContext
context.Add(
new Post
{
Blog = blog,
Title = "What’s next for System.Text.Json?",
Content = ".NET 5.0 was released recently and has come with many..."
});
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
W tym przypadku widok debugowania monitora zmian pokazuje, że wszystkie stany jednostki i modyfikacje właściwości są znane, mimo że nie nastąpiło wykrywanie zmian. Wynika to z faktu PropertyEntry.CurrentValue , że jest to metoda EF Core, co oznacza, że program EF Core natychmiast wie o zmianie wprowadzonej przez tę metodę. Podobnie wywołanie DbContext.Add umożliwia programowi EF Core natychmiastowe poznanie nowej jednostki i odpowiednie śledzenie jej.
Napiwek
Nie próbuj unikać wykrywania zmian, zawsze używając metod EF Core do wprowadzania zmian jednostek. Takie działanie jest często bardziej kłopotliwe i działa mniej dobrze niż wprowadzanie zmian w jednostkach w normalny sposób. Celem tego dokumentu jest informowanie o tym, kiedy jest konieczne wykrywanie zmian, a kiedy nie. Celem nie jest zachęcanie do unikania wykrywania zmian.
Metody, które automatycznie wykrywają zmiany
DetectChanges() metoda jest wywoływana automatycznie przez metody, w których może to mieć wpływ na wyniki. Oto następujące metody:
- DbContext.SaveChanges i DbContext.SaveChangesAsync, aby upewnić się, że wszystkie zmiany zostaną wykryte przed zaktualizowaniem bazy danych.
- ChangeTracker.Entries() i ChangeTracker.Entries<TEntity>(), aby zapewnić aktualność stanów jednostek i zmodyfikowanych właściwości.
- ChangeTracker.HasChanges(), aby upewnić się, że wynik jest dokładny.
- ChangeTracker.CascadeChanges(), aby zapewnić prawidłowe stany jednostek dla jednostek głównych/nadrzędnych przed kaskadą.
- DbSet<TEntity>.Local, aby upewnić się, że śledzony graf jest aktualny.
Istnieją również pewne miejsca, w których wykrywanie zmian odbywa się tylko w pojedynczym wystąpieniu jednostki, a nie na całym grafie śledzonych jednostek. Te miejsca to:
- W przypadku używania polecenia DbContext.Entryupewnij się, że stan i zmodyfikowane właściwości jednostki są aktualne.
- W przypadku używania EntityEntry metod takich jak
Property
,Collection
Reference
lubMember
w celu zapewnienia, że modyfikacje właściwości, bieżące wartości itp. są aktualne. - Gdy jednostka zależna/podrzędna zostanie usunięta, ponieważ wymagana relacja została zerwana. Wykrywa to, kiedy nie należy usuwać jednostki, ponieważ została ona ponownie nadrzędna.
Lokalne wykrywanie zmian dla pojedynczej jednostki może być wyzwalane jawnie przez wywołanie metody EntityEntry.DetectChanges().
Uwaga
Lokalne wykrywanie zmian może przegapić niektóre zmiany, które można znaleźć w pełnym wykryciu. Dzieje się tak, gdy akcje kaskadowe wynikające z niezakrytych zmian w innych jednostkach mają wpływ na określoną jednostkę. W takich sytuacjach aplikacja może wymagać wymuszenia pełnego skanowania wszystkich jednostek przez jawne wywołanie metody ChangeTracker.DetectChanges().
Wyłączanie automatycznego wykrywania zmian
Wydajność wykrywania zmian nie jest wąskim gardłem dla większości aplikacji. Jednak wykrywanie zmian może stać się problemem z wydajnością niektórych aplikacji, które śledzą tysiące jednostek. (Dokładna liczba będzie zależeć od wielu elementów, takich jak liczba właściwości w jednostce). Z tego powodu automatyczne wykrywanie zmian można wyłączyć przy użyciu polecenia ChangeTracker.AutoDetectChangesEnabled. Rozważmy na przykład przetwarzanie jednostek sprzężenia w relacji wiele-do-wielu z ładunkami:
public override int SaveChanges()
{
foreach (var entityEntry in ChangeTracker.Entries<PostTag>()) // Detects changes automatically
{
if (entityEntry.State == EntityState.Added)
{
entityEntry.Entity.TaggedBy = "ajcvickers";
entityEntry.Entity.TaggedOn = DateTime.Now;
}
}
try
{
ChangeTracker.AutoDetectChangesEnabled = false;
return base.SaveChanges(); // Avoid automatically detecting changes again here
}
finally
{
ChangeTracker.AutoDetectChangesEnabled = true;
}
}
Jak wiemy z poprzedniej sekcji, zarówno, jak ChangeTracker.Entries<TEntity>() i DbContext.SaveChanges automatycznie wykrywają zmiany. Jednak po wywołaniu pozycji kod nie wprowadza żadnych zmian stanu jednostki ani właściwości. (Ustawienie normalnych wartości właściwości w dodanych jednostkach nie powoduje żadnych zmian stanu). W związku z tym kod wyłącza niepotrzebne automatyczne wykrywanie zmian podczas wywoływania do podstawowej metody SaveChanges. Kod korzysta również z bloku try/finally, aby upewnić się, że ustawienie domyślne zostanie przywrócone, nawet jeśli funkcja SaveChanges zakończy się niepowodzeniem.
Napiwek
Nie zakładaj, że kod musi wyłączyć automatyczne wykrywanie zmian w celu zapewnienia dobrego działania. Jest to konieczne tylko wtedy, gdy profilowanie aplikacji śledzącej wiele jednostek wskazuje, że wydajność wykrywania zmian jest problemem.
Wykrywanie zmian i konwersji wartości
Aby korzystać ze śledzenia zmian migawki z typem jednostki, program EF Core musi mieć następujące możliwości:
- Tworzenie migawki każdej wartości właściwości podczas śledzenia jednostki
- Porównaj tę wartość z bieżącą wartością właściwości
- Generowanie kodu skrótu dla wartości
Jest to obsługiwane automatycznie przez program EF Core dla typów, które można bezpośrednio zamapować do bazy danych. Jednak gdy konwerter wartości jest używany do mapowania właściwości, ten konwerter musi określić sposób wykonywania tych akcji. Jest to osiągane za pomocą porównania wartości i zostało szczegółowo opisane w dokumentacji funkcji porównywania wartości.
Jednostki powiadomień
W przypadku większości aplikacji zalecane jest śledzenie zmian migawek. Jednak aplikacje, które śledzą wiele jednostek i/lub wprowadzają wiele zmian w tych jednostkach, mogą korzystać z implementacji jednostek, które automatycznie powiadamiają program EF Core o zmianie ich właściwości i wartości nawigacji. Są one nazywane "jednostkami powiadomień".
Implementowanie jednostek powiadomień
Jednostki powiadomień korzystają z INotifyPropertyChanging interfejsów i INotifyPropertyChanged , które są częścią biblioteki klas bazowych platformy .NET (BCL). Te interfejsy definiują zdarzenia, które muszą zostać wyzwolone przed zmianą wartości właściwości i po jej zmianie. Przykład:
public class Blog : INotifyPropertyChanging, INotifyPropertyChanged
{
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
public int Id
{
get => _id;
set
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Id)));
_id = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
}
}
private string _name;
public string Name
{
get => _name;
set
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}
Ponadto wszystkie nawigacje kolekcji muszą implementować INotifyCollectionChanged
; w powyższym przykładzie jest to zadowalające przy użyciu wpisów ObservableCollection<T> . Program EF Core jest również dostarczany z implementacją ObservableHashSet<T> , która ma bardziej wydajne wyszukiwanie kosztem stabilnego zamawiania.
Większość tego kodu powiadomień jest zwykle przenoszona do niezamapowanej klasy bazowej. Przykład:
public class Blog : NotifyingEntity
{
private int _id;
public int Id
{
get => _id;
set => SetWithNotify(value, out _id);
}
private string _name;
public string Name
{
get => _name;
set => SetWithNotify(value, out _name);
}
public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}
public abstract class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged
{
protected void SetWithNotify<T>(T value, out T field, [CallerMemberName] string propertyName = "")
{
NotifyChanging(propertyName);
field = value;
NotifyChanged(propertyName);
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private void NotifyChanging(string propertyName)
=> PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
Konfigurowanie jednostek powiadomień
Nie ma możliwości sprawdzenia, czy INotifyPropertyChanging
program EF Core jest INotifyPropertyChanged
w pełni zaimplementowany do użytku z platformą EF Core. W szczególności niektóre z tych interfejsów robią to z powiadomieniami tylko dla niektórych właściwości, a nie we wszystkich właściwościach (w tym nawigacji) zgodnie z wymaganiami platformy EF Core. Z tego powodu program EF Core nie jest automatycznie podłączany do tych zdarzeń.
Zamiast tego należy skonfigurować program EF Core do korzystania z tych jednostek powiadomień. Zwykle odbywa się to dla wszystkich typów jednostek przez wywołanie metody ModelBuilder.HasChangeTrackingStrategy. Przykład:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
}
(Strategię można również ustawić inaczej dla różnych typów jednostek przy użyciu metody EntityTypeBuilder.HasChangeTrackingStrategy, ale zwykle jest to sprzeczne z produktem, ponieważ funkcja DetectChanges jest nadal wymagana dla tych typów, które nie są jednostkami powiadomień).
Pełne śledzenie zmian powiadomień wymaga zaimplementowania obu INotifyPropertyChanging
tych elementów i INotifyPropertyChanged
. Umożliwia to zapisanie oryginalnych wartości tuż przed zmianą wartości właściwości, co pozwala uniknąć konieczności utworzenia migawki przez program EF Core podczas śledzenia jednostki. Typy jednostek implementujące INotifyPropertyChanged
tylko mogą być używane z programem EF Core. W tym przypadku program EF nadal tworzy migawkę podczas śledzenia jednostki w celu śledzenia oryginalnych wartości, ale następnie używa powiadomień do natychmiastowego wykrywania zmian, a nie konieczności wywoływanie funkcji DetectChanges.
Różne ChangeTrackingStrategy wartości zostały podsumowane w poniższej tabeli.
ChangeTrackingStrategy | Wymagane interfejsy | Wymaga wykrywania zmian | Migawki oryginalnych wartości |
---|---|---|---|
Snapshot | None | Tak | Tak |
ZmienionoNotyfikacje | Inotifypropertychanged | Nie. | Tak |
ZmianaandChangedNotifications | INotifyPropertyChanged i INotifyPropertyChanging | Nie. | Nie. |
ChangingAndChangedNotificationsWithOriginalValues | INotifyPropertyChanged i INotifyPropertyChanging | Nie. | Tak |
Korzystanie z jednostek powiadomień
Jednostki powiadomień zachowują się jak inne jednostki, z tą różnicą, że wprowadzanie zmian w wystąpieniach jednostki nie wymaga wywołania w celu ChangeTracker.DetectChanges() wykrycia tych zmian. Przykład:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
// Change a property value
blog.Name = ".NET Blog (Updated!)";
// Add a new entity to a navigation
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..."
});
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
W przypadku normalnych jednostek widok debugowania monitora zmian pokazał, że te zmiany nie zostały wykryte do momentu wywołania funkcji DetectChanges. Patrząc na widok debugowania, gdy używane są jednostki powiadomień, widać, że te zmiany zostały wykryte natychmiast:
Blog {Id: 1} Modified
Id: 1 PK
Name: '.NET Blog (Updated!)' Modified
Posts: [{Id: 1}, {Id: 2}, {Id: -2147482643}]
Post {Id: -2147482643} Added
Id: -2147482643 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} 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}
Serwery proxy śledzenia zmian
Program EF Core może dynamicznie generować typy serwerów proxy implementujące INotifyPropertyChanging i INotifyPropertyChanged. Wymaga to zainstalowania pakietu NuGet Microsoft.EntityFrameworkCore.Proxies i włączenia serwerów UseChangeTrackingProxies proxy śledzenia zmian za pomocą polecenia Na przykład:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseChangeTrackingProxies();
Tworzenie dynamicznego serwera proxy obejmuje utworzenie nowego, dynamicznego typu platformy .NET (przy użyciu implementacji serwerów proxy Castle.Core ), który dziedziczy z typu jednostki, a następnie zastępuje wszystkie zestawy właściwości. W związku z tym typy jednostek dla serwerów proxy muszą być typami, które mogą być dziedziczone z i muszą mieć właściwości, które mogą być zastępowane. Ponadto utworzone jawnie nawigacje kolekcji muszą implementować INotifyCollectionChanged na przykład:
public class Blog
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Post> Posts { get; } = new ObservableCollection<Post>();
}
public class Post
{
public virtual int Id { get; set; }
public virtual string Title { get; set; }
public virtual string Content { get; set; }
public virtual int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
Jednym z znaczących wad śledzenia zmian serwerów proxy jest to, że program EF Core musi zawsze śledzić wystąpienia serwera proxy, nigdy wystąpienia bazowego typu jednostki. Dzieje się tak, ponieważ wystąpienia bazowego typu jednostki nie będą generować powiadomień, co oznacza, że zmiany wprowadzone w tych jednostkach zostaną pominięte.
Program EF Core automatycznie tworzy wystąpienia serwera proxy podczas wykonywania zapytań względem bazy danych, więc ta wada jest zwykle ograniczona do śledzenia nowych wystąpień jednostek. Te wystąpienia muszą być tworzone przy użyciu CreateProxy metod rozszerzeń, a nie w normalny sposób przy użyciu metody new
. Oznacza to, że kod z poprzednich przykładów musi teraz używać elementu CreateProxy
:
using var context = new BlogsContext();
var blog = context.Blogs.Include(e => e.Posts).First(e => e.Name == ".NET Blog");
// Change a property value
blog.Name = ".NET Blog (Updated!)";
// Add a new entity to a navigation
blog.Posts.Add(
context.CreateProxy<Post>(
p =>
{
p.Title = "What’s next for System.Text.Json?";
p.Content = ".NET 5.0 was released recently and has come with many...";
}));
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Zdarzenia śledzenia zmian
Program EF Core uruchamia ChangeTracker.Tracked zdarzenie, gdy jednostka jest śledzona po raz pierwszy. Przyszłe zmiany stanu jednostki powodują zdarzenia ChangeTracker.StateChanged . Aby uzyskać więcej informacji, zobacz Zdarzenia platformy .NET w programie EF Core.
Uwaga
Zdarzenie StateChanged
nie jest wyzwalane, gdy jednostka jest najpierw śledzona, mimo że stan zmienił się z Detached
na jeden z innych stanów. Pamiętaj, aby nasłuchiwać zarówno zdarzeń, StateChanged
jak i Tracked
, aby otrzymywać wszystkie odpowiednie powiadomienia.