Další funkce sledování změn

Tento dokument popisuje různé funkce a scénáře zahrnující sledování změn.

Návod

Tento dokument předpokládá, že stavy entit a základy sledování změn EF Core jsou srozumitelné. Další informace o těchto tématech najdete v tématu Sledování změn v EF Core .

Návod

Veškerý kód v tomto dokumentu můžete spustit a ladit stažením ukázkového kódu z GitHubu.

Add proti AddAsync

Entity Framework Core (EF Core) poskytuje asynchronní metody při každém použití této metody může vést k interakci s databází. Synchronní metody jsou také poskytovány, aby se zabránilo režii při použití databází, které nepodporují vysoký výkon asynchronního přístupu.

DbContext.Add a DbSet<TEntity>.Add obvykle nepřistupují k databázi, protože tyto metody ze své podstaty začínají sledovat entity. Některé formy generování hodnot však mohou získat přístup k databázi, aby se vygenerovala hodnota klíče. Jediný generátor hodnot, který to dělá a je dodáván s EF Core, je HiLoValueGenerator<TValue>. Použití tohoto generátoru je neobvyklé; ve výchozím nastavení se nenakonfiguruje. To znamená, že velká většina aplikací by měla používat Add, a ne AddAsync.

Jiné podobné metody jako Update, Attacha Remove nemají asynchronní přetížení, protože nikdy negenerují nové hodnoty klíče, a proto nikdy nepotřebují přístup k databázi.

AddRange, UpdateRange, AttachRange a RemoveRange

DbSet<TEntity>a DbContext poskytují alternativní verze , AddUpdate, Attacha Remove které přijímají více instancí v jednom volání. Tyto metody jsou AddRange, UpdateRange, AttachRangea RemoveRange v uvedeném pořadí.

Tyto metody jsou poskytovány pro usnadnění. Použití metody "range" má stejnou funkci jako více volání ekvivalentní metody bez funkce rozsahu. Mezi těmito dvěma přístupy není žádný významný rozdíl mezi výkonem.

Poznámka:

To je jiné než v EF6, kde AddRange i Add automaticky volaly DetectChanges, ale při volání Add několikrát způsobilo, že DetectChanges byla volána vícekrát místo jednou. To v EF6 zefektivnilo AddRange . V EF Core ani jedna z těchto metod automaticky nevolají DetectChanges.

DbContext versus metody DbSet

Mnoho metod, včetně Add, Update, Attacha Remove, mají implementace na obou DbSet<TEntity> a DbContext. Tyto metody mají naprosto stejné chování pro normální typy entit. Důvodem je to, že typ CLR entity je namapován na jeden a pouze jeden typ entity v modelu EF Core. Proto typ CLR plně definuje, kde entita zapadá do modelu, a proto dbSet, který se má použít, lze určit implicitně.

Výjimkou tohoto pravidla je použití typů entit sdíleného typu, které se primárně používají pro entity spojení mnoha-na-mnoho. Při použití typu entity sdíleného typu musí být nejprve vytvořena sada DbSet pro typ modelu EF Core, který se používá. Metody jako Add, Update, Attacha Remove pak lze použít v DbSet bez nejednoznačnosti, jak se používá typ modelu EF Core.

Sdílené typy entit se ve výchozím nastavení používají pro spojené entity v relacích M:N. Typ entity sdíleného typu lze také explicitně nakonfigurovat pro použití v relaci M:N. Například následující kód konfiguruje Dictionary<string, int> jako typ vazební entity:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

Změna cizích klíčů a navigace ukazuje, jak přidružit dvě entity sledováním nové instance entity join. Následující kód to dělá pro Dictionary<string, int> typ entity sdíleného typu, který se používá pro entitu join:

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(e => e.Id == 1);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

Všimněte si, že DbContext.Set<TEntity>(String) se používá k vytvoření sady DbSet pro PostTag typ entity. Sadu DbSet pak můžete použít k tomu, abyste zavolali Add s instancí nové spojovací entity.

Důležité

Typ CLR používaný pro typy entit spojení podle konvence se může v budoucích verzích změnit, aby se zlepšil výkon. Nezávisejte na žádném konkrétním typu entity spojení, pokud není explicitně nakonfigurovaný tak, jak je to provedeno Dictionary<string, int> v kódu výše.

Přístup k vlastnostem versus přístup k poli

Přístup k vlastnostem entity ve výchozím nastavení používá záložní pole vlastnosti. To je efektivní a zabraňuje aktivaci vedlejších účinků volání metod getter a setter. Takto se například opožděné načítání dokáže vyhnout aktivaci nekonečných smyček. Další informace o konfiguraci pomocných polí v modelu najdete v části Backing Fields.

Někdy může být žádoucí, aby EF Core generoval vedlejší účinky při úpravě hodnot vlastností. Například při datové vazbě k entitám, když nastavíte vlastnost, může vygenerovat oznámení do uživatelského rozhraní, což se nestane, když nastavíte pole přímo. Toho lze dosáhnout změnou hodnoty PropertyAccessMode na:

Režimy přístupu k vlastnostem Field a PreferField způsobí, že EF Core získá přístup k hodnotě vlastnosti prostřednictvím jejich podporujícího pole. Stejně tak Property a PreferProperty způsobí, že EF Core získá přístup k hodnotě vlastnosti prostřednictvím svého getteru a setteru.

Pokud se Field nebo Property používají a EF Core nemá přístup k hodnotě prostřednictvím pole, nebo vlastnosti pomocí getteru/setteru, EF Core vyvolá výjimku. Tím se zajistí, že EF Core vždy používá přístup k polím nebo vlastnostem, jak očekáváte.

Na druhou stranu se režimy PreferField a režimy PreferProperty vrátí k použití vlastnosti nebo podpůrného pole, pokud není možné použít upřednostňovaný přístup. PreferField je výchozí. To znamená, že EF Core bude používat pole, kdykoli to bude možné, ale neselže, pokud je třeba se k vlastnosti dostat prostřednictvím jejího getteru nebo setteru.

FieldDuringConstruction a PreferFieldDuringConstruction nakonfigurujte EF Core tak, aby používala backingová pole pouze při vytváření instancí entit. To umožňuje spouštění dotazů bez nežádoucích účinků getter a setter, zatímco pozdější změny vlastností EF Core způsobí tyto vedlejší účinky.

Různé režimy přístupu k vlastnostem jsou shrnuty v následující tabulce:

RežimPřístupuKVlastnostem Přednost Vytváření preferovaných entit Záložní řešení Záložní vytváření entit
Field Pole Pole Vyvolá Vyvolání výjimek
Property Vlastnictví Vlastnictví Výjimky Vyvolání výjimek
PreferField Pole Pole Vlastnictví Vlastnictví
PreferProperty Vlastnictví Vlastnictví Pole Pole
FieldDuringConstruction Vlastnictví Pole Pole Výjimky
PreferFieldDuringConstruction Vlastnictví Pole Pole Vlastnictví

Dočasné hodnoty

EF Core vytvoří dočasné hodnoty klíče při sledování nových entit, které budou mít skutečné hodnoty klíče generované databází při volání SaveChanges. Přehled o tom, jak se tyto dočasné hodnoty používají, najdete v tématu Sledování změn v EF Core .

Přístup k dočasným hodnotám

Dočasné hodnoty jsou uloženy v sledování změn a nejsou nastaveny přímo na instance entit. Tyto dočasné hodnoty jsou však zpřístupněny při použití různých mechanismů pro přístup ke sledovaným entitám. Například následující kód přistupuje k dočasné hodnotě pomocí EntityEntry.CurrentValues:

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

Výstupem tohoto kódu je:

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary lze použít ke kontrole dočasných hodnot.

Manipulace s dočasnými hodnotami

Někdy je užitečné explicitně pracovat s dočasnými hodnotami. Například kolekce nových entit může být vytvořena na webovém klientovi a následně serializována zpět na server. Hodnoty cizího klíče představují jeden ze způsobů, jak nastavit vztahy mezi těmito entitami. Následující kód tento přístup používá k přidružení grafu nových entit cizím klíčem, zatímco stále umožňuje vygenerovat skutečné hodnoty klíče při zavolání SaveChanges.

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -1,
        Title = "Announcing the Release of EF Core 5.0",
        Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
    },
    new Post
    {
        Id = -2,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Všimněte si, že:

  • Záporná čísla se používají jako dočasné hodnoty klíče; to není nutné, ale jedná se o běžnou konvenci, která brání klíčovým konfliktům.
  • Vlastnost Post.BlogId FK je přiřazena stejnou zápornou hodnotu jako PK přidruženého blogu.
  • Hodnoty PK jsou označené jako dočasné nastavením IsTemporary po sledování každé entity. To je nezbytné, protože jakákoli hodnota klíče zadaná aplikací se předpokládá jako skutečná hodnota klíče.

Zobrazení ladění sledování změn před voláním saveChanges ukazuje, že hodnoty PK jsou označené jako dočasné a příspěvky jsou přidružené ke správným blogům, včetně opravy navigace:

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  BlogId: -2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: -2}
  Tags: []
Post {Id: -1} Added
  Id: -1 PK Temporary
  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}

Po volání SaveChangesbyly tyto dočasné hodnoty nahrazeny skutečnými hodnotami vygenerovanými databází:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{Id: 2}]
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}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []

Práce s výchozími hodnotami

EF Core umožňuje, aby vlastnost získala výchozí hodnotu z databáze, když SaveChanges je volána. Stejně jako u vygenerovaných hodnot klíčů ef Core použije výchozí hodnotu pouze z databáze, pokud nebyla explicitně nastavena žádná hodnota. Představte si například následující typ entity:

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

Vlastnost ValidFrom je nakonfigurovaná tak, aby z databáze získala výchozí hodnotu:

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

Když vložíte entitu tohoto typu, EF Core nechá databázi vygenerovat hodnotu, pokud nebyla nastavena explicitní hodnota. Například:

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

await context.SaveChangesAsync();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Při pohledu na debugovací zobrazení sledování změn vidíte, že první token byl vygenerován databází, zatímco druhý token použil hodnotu výslovně nastavenou:

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

Poznámka:

Použití výchozích hodnot databáze vyžaduje, aby byl ve sloupci databáze nakonfigurované omezení výchozí hodnoty. To se provádí automaticky migrací EF Core při použití HasDefaultValueSql nebo HasDefaultValue. Pokud nepoužíváte migrace EF Core, nezapomeňte u sloupce vytvořit výchozí omezení jiným způsobem.

Použití nullovatelných vlastností

EF Core dokáže určit, zda byla vlastnost nastavena porovnáním hodnoty vlastnosti s výchozím nastavením CLR pro daný typ. Ve většině případů to funguje dobře, ale znamená to, že výchozí nastavení CLR nelze explicitně vložit do databáze. Představte si například entitu s celočíselnou vlastností:

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

Pokud je tato vlastnost nakonfigurovaná tak, aby měla výchozí hodnotu databáze -1:

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

Záměrem je, aby se výchozí hodnota -1 použila vždy, když není nastavena explicitní hodnota. Nastavení hodnoty 0 (výchozí hodnota CLR pro celá čísla) je však nerozlišitelné od EF Core od nastavení žádné hodnoty, to znamená, že pro tuto vlastnost není možné vložit hodnotu 0. Například:

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

Všimněte si, že instance, kde Count byla explicitně nastavena na 0, stále získává výchozí hodnotu z databáze, což není to, co jsme chtěli. Snadným způsobem, jak to vyřešit, je nastavit Count vlastnost nullable:

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

Díky tomu bude výchozí hodnota CLR null místo 0, což znamená, že při explicitním nastavení se teď vloží hodnota 0.

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Použití záložních polí s možnou hodnotou null

Problém s nastavením vlastnosti jako nulovatelné je, že může se stát, že nebude koncepčně nulovatelná v doménovém modelu. Vynucení, aby byla vlastnost nulovatelná, proto ohrožuje model.

Vlastnost může být ponechána jako nenulovatelná, přičemž pouze podpůrné pole je nulovatelné. Například:

public class Foo3
{
    public int Id { get; set; }

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

To umožňuje vložit výchozí hodnotu CLR (0), pokud je vlastnost explicitně nastavena na hodnotu 0, a přitom není nutné vystavit vlastnost jako nullable v doménovém modelu. Například:

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
await context.SaveChangesAsync();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Zálohovaná pole s možnou hodnotou null pro logické vlastnosti

Tento vzor je zvlášť užitečný při použití logických vlastností (typ bool) s výchozími nastaveními vygenerovanými databází. Vzhledem k tomu, že výchozí hodnota bool CLR je "false", znamená to, že "false" nelze explicitně vložit pomocí normálního vzoru. Například uvažujme typ entity User:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

Vlastnost IsAuthorized je nakonfigurována s výchozí hodnotou databáze true:

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

IsAuthorized Vlastnost lze před vložením nastavit na true nebo false nebo ji můžete nechat bez nastavení, v takovém případě se použije výchozí nastavení databáze:

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

await context.SaveChangesAsync();

Výstup z SaveChanges při použití SQLite ukazuje, že výchozí hodnota databáze se používá pro Mac, zatímco explicitní hodnoty jsou nastaveny pro Alice a Baxter:

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Pouze výchozí nastavení schématu

Někdy je užitečné mít výchozí hodnoty ve schématu databáze vytvořené migrací EF Core, aniž by EF Core někdy používal tyto hodnoty pro vložení. Toho lze dosáhnout konfigurací vlastnosti například PropertyBuilder.ValueGeneratedNever :

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();