Detekce změn a oznámení

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 článek je popsaný v nástroji Change Tracking v EF Core a v tomto dokumentu se předpokládá, že se rozumí stavy entit a základy sledování změn Entity Framework Core (EF Core).

Sledování změn vlastností a relací vyžaduje, aby dbContext mohl tyto změny rozpoznat. Tento dokument popisuje, jak k této detekci dochází, a také o tom, jak používat oznámení vlastností nebo proxy servery pro sledování změn k vynucení okamžité detekce změn.

Tip

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

Sledování změn snímků

Ef Core ve výchozím nastavení vytvoří snímek hodnot vlastností každé entity při prvním sledování instancí DbContext. Hodnoty uložené v tomto snímku se pak porovnávají s aktuálními hodnotami entity, aby bylo možné určit, které hodnoty vlastností se změnily.

K této detekci změn dochází, když je volána funkce SaveChanges, aby se před odesláním aktualizací do databáze zjistily všechny změněné hodnoty. Detekce změn se ale provádí i v jiných případech, aby aplikace fungovala s aktuálními informacemi o sledování. Detekce změn může být vynucena kdykoli voláním ChangeTracker.DetectChanges().

Pokud je potřeba detekce změn

Detekce změn je nutná v případě, že došlo ke změně vlastnosti nebo navigace bez použití EF Core k provedení této změny. Zvažte například načtení blogů a příspěvků a následné změny těchto entit:

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

Zobrazení ladění sledování změn před voláním ChangeTracker.DetectChanges() ukazuje, že provedené změny nebyly zjištěny, a proto se neprojevují ve stavech entit a upravených datech vlastností:

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}

Konkrétně stav příspěvku blogu je stále Unchangeda nový příspěvek se nezobrazuje jako sledovaný entita. (Astute si všimne, že vlastnosti hlásí nové hodnoty, i když ef Core ještě tyto změny nezjistil. Důvodem je to, že zobrazení ladění čte aktuální hodnoty přímo z instance entity.)

Porovnejte to se zobrazením ladění po volání 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}

Nyní blog je správně označen jako Modified a nový příspěvek byl zjištěn a je sledován jako Added.

Na začátku této části jsme uvedli, že zjišťování změn je potřeba, když k provedení této změny nepoužíváte EF Core. To se děje ve výše uvedeném kódu. To znamená, že změny vlastnosti a navigace jsou provedeny přímo na instancích entit, a ne pomocí žádné metody EF Core.

Porovnejte to s následujícím kódem, který upravuje entity stejným způsobem, ale tentokrát pomocí 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);

V tomto případě zobrazení ladění sledování změn ukazuje, že jsou známé všechny stavy entit a úpravy vlastností, i když detekce změn neproběhla. Důvodem je to, že PropertyEntry.CurrentValue se jedná o metodu EF Core, což znamená, že EF Core okamžitě ví o změně provedené touto metodou. Podobně volání DbContext.Add umožňuje EF Core okamžitě vědět o nové entitě a správně ji sledovat.

Tip

Nepokoušejte se vyhnout detekci změn pomocí metod EF Core k provedení změn entit. To je často těžkopádnější a funguje méně dobře než provádění změn entit běžným způsobem. Záměrem tohoto dokumentu je informovat o tom, kdy je potřeba zjišťovat změny a kdy ne. Záměrem není podpořit předcházení detekci změn.

Metody, které automaticky detekují změny

DetectChanges() se volá automaticky metodami, ve kterých to pravděpodobně ovlivní výsledky. Tyto metody jsou:

Existuje také několik míst, kde se detekce změn odehrává pouze u jedné instance entity, nikoli v celém grafu sledovaných entit. Tato místa jsou:

  • Při použití DbContext.Entryzajistíte, aby stav a změněné vlastnosti entity byly aktuální.
  • Při použití EntityEntry metod, jako Propertyje , ReferenceCollectionnebo Member k zajištění úprav vlastností, aktuálních hodnot atd. jsou aktuální.
  • Když se odstraní závislá nebo podřízená entita, protože došlo k odstranění požadované relace. Zjistí se, kdy by se entita neměla odstranit, protože byla znovu nadřazená.

Místní detekce změn pro jednu entitu se dá aktivovat explicitně voláním EntityEntry.DetectChanges().

Poznámka

Místní detekce změn může vynechat některé změny, které by bylo možné najít v úplné detekci. K tomu dochází v případě, že kaskádové akce vyplývající z nedetekovaných změn jiných entit mají vliv na danou entitu. V takových situacích může aplikace muset vynutit úplné prohledávání všech entit explicitním voláním ChangeTracker.DetectChanges().

Zakázání automatické detekce změn

Výkon detekce změn není kritickým bodem většiny aplikací. Detekce změn se ale může stát problémem s výkonem u některých aplikací, které sledují tisíce entit. (Přesné číslo bude záviset na mnoha věcech, například na počtu vlastností v entitě.) Z tohoto důvodu lze automatickou detekci změn zakázat pomocí ChangeTracker.AutoDetectChangesEnabled. Zvažte například zpracování entit spojení v relaci M:N s datovými částmi:

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 víme z předchozí části, obě ChangeTracker.Entries<TEntity>() a DbContext.SaveChanges automaticky detekují změny. Po volání položek však kód neprovádí žádné změny stavu entity nebo vlastnosti. (Nastavení normálních hodnot vlastností u přidaných entit nezpůsobí žádné změny stavu.) Kód proto zakáže zbytečné automatické zjišťování změn při volání do základní metody SaveChanges. Kód také používá blok try/finally, aby se zajistilo, že se obnoví výchozí nastavení i v případě, že se funkce SaveChanges nezdaří.

Tip

Nepředpokládáte, že váš kód musí zakázat automatické zjišťování změn, aby fungoval dobře. To je potřeba jenom v případě, že profilace aplikace sledující mnoho entit značí, že je problém s výkonem detekce změn.

Detekce změn a převodů hodnot

Pokud chcete použít sledování změn snímků s typem entity, musí být EF Core schopné:

  • Vytvoření snímku každé hodnoty vlastnosti při sledování entity
  • Porovnejte tuto hodnotu s aktuální hodnotou vlastnosti.
  • Vygenerování kódu hash pro hodnotu

Ef Core to zpracovává automaticky pro typy, které je možné přímo namapovat na databázi. Pokud se však k mapování vlastnosti používá převaděč hodnot, musí tento převaděč určit, jak tyto akce provést. Toho dosáhnete pomocí porovnávače hodnot a podrobně je popsáno v dokumentaci k porovnání hodnot.

Entity oznámení

Sledování změn snímků se doporučuje pro většinu aplikací. Aplikace, které sledují mnoho entit nebo provádějí mnoho změn těchto entit, ale můžou mít prospěch z implementace entit, které ef Core automaticky upozorňují na změnu jejich vlastností a navigačních hodnot. Tyto entity se označují jako "entity oznámení".

Implementace entit oznámení

Entity oznámení využívají INotifyPropertyChanging rozhraní a INotifyPropertyChanged rozhraní, která jsou součástí knihovny základních tříd .NET (BCL). Tato rozhraní definují události, které se musí aktivovat před a po změně hodnoty vlastnosti. Příklad:

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

Kromě toho musí být implementovány INotifyCollectionChangedvšechny navigace kolekce ; v příkladu výše je splněn pomocí ObservableCollection<T> příspěvku. EF Core se také dodává s ObservableHashSet<T> implementací, která má efektivnější vyhledávání na úkor stabilního řazení.

Většina tohoto kódu oznámení se obvykle přesune do nemapované základní třídy. Příklad:

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

Konfigurace entit oznámení

Ef Core neexistuje způsob, jak ověřit, že INotifyPropertyChanging nebo INotifyPropertyChanged jsou plně implementovány pro použití s EF Core. Zejména některé použití těchto rozhraní to dělají s oznámeními pouze na určitých vlastnostech, nikoli u všech vlastností (včetně navigace), jak to vyžaduje EF Core. Z tohoto důvodu se EF Core automaticky nepřichytá k těmto událostem.

Místo toho musí být EF Core nakonfigurované tak, aby používaly tyto entity oznámení. To se obvykle provádí pro všechny typy entit voláním ModelBuilder.HasChangeTrackingStrategy. Příklad:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotifications);
}

(Strategii lze také nastavit jinak pro různé typy entit, které používají EntityTypeBuilder.HasChangeTrackingStrategy, ale obvykle je to kontraproduktivní, protože Funkce DetectChanges je stále nutná pro tyto typy, které nejsou entitami oznámení.)

Úplné sledování změn oznámení vyžaduje, aby byly implementovány obě INotifyPropertyChanging i INotifyPropertyChanged implementované. To umožňuje, aby se původní hodnoty ukládaly těsně před změnou hodnoty vlastnosti, abyste nemuseli ef Core při sledování entity vytvářet snímek. Typy entit, které implementují pouze, je možné použít i INotifyPropertyChanged s EF Core. V tomto případě EF stále vytvoří snímek při sledování entity, aby bylo možné sledovat původní hodnoty, ale pak pomocí oznámení detekuje změny okamžitě, a nemusí být volána funkce DetectChanges.

Různé ChangeTrackingStrategy hodnoty jsou shrnuty v následující tabulce.

ChangeTrackingStrategy Potřebná rozhraní Needs DetectChanges Snímky původních hodnot
Snímek Žádné Ano Ano
ChangedNotifications Inotifypropertychanged Číslo Ano
ChangingAndChangedNotifications INotifyPropertyChanged a INotifyPropertyChanging Číslo Číslo
ChangingAndChangedNotificationsWithOriginalValues INotifyPropertyChanged a INotifyPropertyChanging Číslo Ano

Použití entit oznámení

Entity oznámení se chovají stejně jako jakékoli jiné entity s tím rozdílem, že provádění změn instancí entit nevyžaduje volání, které by ChangeTracker.DetectChanges() tyto změny detekovalo. Příklad:

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

Při normálních entitách zobrazení ladění sledování změn ukázalo, že tyto změny nebyly zjištěny, dokud nebyla volána funkce DetectChanges. Při použití entit oznámení se při zobrazení ladění zobrazí, že se tyto změny zjistily okamžitě:

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}

Proxy servery pro sledování změn

EF Core může dynamicky generovat typy proxy, které implementují INotifyPropertyChanging a INotifyPropertyChanged. To vyžaduje instalaci balíčku NuGet Microsoft.EntityFrameworkCore.Proxies a povolení proxy serverů pro sledování změn, UseChangeTrackingProxies například:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseChangeTrackingProxies();

Vytvoření dynamického proxy serveru zahrnuje vytvoření nového dynamického typu .NET (pomocí implementace proxy serverů Castle.Core ), který dědí z typu entity a pak přepíše všechny setter vlastností. Typy entit pro proxy proto musí být typy, ze kterých lze dědit, a musí mít vlastnosti, které lze přepsat. Také navigace kolekcí vytvořené explicitně musí implementovat INotifyCollectionChanged například:

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

Jednou z významných nevýhod proxy serverů pro sledování změn je, že EF Core musí vždy sledovat instance proxy serveru, nikdy instance základního typu entity. Důvodem je to, že instance základního typu entity nebudou generovat oznámení, což znamená, že změny provedené v těchto entitách budou zmeškané.

EF Core vytváří proxy instance automaticky při dotazování databáze, takže tato nevýhoda je obecně omezená na sledování nových instancí entit. Tyto instance musí být vytvořeny pomocí CreateProxy rozšiřujících metod, a ne normálním způsobem pomocí new. To znamená, že kód z předchozích příkladů teď musí používat 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);

Události sledování změn

EF Core aktivuje ChangeTracker.Tracked událost při prvním sledování entity. Budoucí změny stavu entity vedou k událostem ChangeTracker.StateChanged . Další informace najdete v tématu Události .NET v EF Core.

Poznámka

Událost StateChanged se neaktivuje, když je entita poprvé sledována, i když se stav změnil z Detached jednoho z dalších stavů. Nezapomeňte naslouchat událostem a StateChangedTracked dostávat všechna relevantní oznámení.