Sdílet prostřednictvím


Další funkce sledování změn

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

Tip

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 .

Tip

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

Add versus 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 dodává 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, AttachRangea 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 jako pohodlí. Použití metody "range" má stejnou funkci jako více volání ekvivalentní metody bez rozsahu. Mezi těmito dvěma přístupy není žádný významný rozdíl mezi výkonem.

Poznámka

To se liší od EF6, kde AddRange i Add oba automaticky volají DetectChanges, ale volání Add několikrát způsobilo, že DetectChanges být volána vícekrát místo jednou. To v EF6 zefektivnilo AddRange . V EF Core žádná z těchto metod automaticky volat 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í M:N. 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.

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

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. Tuto sadu DbSet pak můžete použít k volání Add s novou instancí entity join.

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 a 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 backingových polí v modelu najdete v tématu Backing Fields(Backing Fields ).

Někdy může být žádoucí, aby EF Core generoval vedlejší účinky při úpravě hodnot vlastností. Když například datové vazby k entitám nastavíte vlastnost, může vygenerovat oznámení do USA, ke kterým nedojde při přímém nastavení pole. Toho lze dosáhnout změnou PropertyAccessMode hodnoty for:

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

Pokud Field nebo se používají a Property EF Core nemá přístup k hodnotě prostřednictvím pole nebo vlastnosti getter/setter, ef Core vyvolá výjimku. Tím se zajistí, že EF Core vždy používá přístup k polím nebo vlastnostem, když si myslíte, že je.

Na druhou stranu PreferField se režimy a PreferProperty režimy vrátí k použití vlastnosti nebo backingového pole, pokud není možné použít upřednostňovaný přístup. PreferField je výchozí možnost. To znamená, že EF Core bude používat pole, kdykoli to může, ale nebude neúspěšné, pokud musí být vlastnost přístupná prostřednictvím svého getteru nebo setter.

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:

PropertyAccessMode Předvoleb Preference vytváření entit Náhradní téma Záložní vytváření entit
Field Pole Pole Vyvolá Vyvolá
Property Vlastnost Vlastnost Vyvolá Vyvolá
PreferField Pole Pole Vlastnost Vlastnost
PreferProperty Vlastnost Vlastnost Pole Pole
FieldDuringConstruction Vlastnost Pole Pole Vyvolá
PreferFieldDuringConstruction Vlastnost Pole Pole Vlastnost

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. Pří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 zobrazení ladění sledování změn vidíte, že první token vygeneroval ValidFrom databáze, zatímco druhý token použil hodnotu explicitně 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í vlastností s možnou hodnotou null

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. Pří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í sadě 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 vytvářením vlastnosti null, že nemusí být koncepčně nullable v doménovém modelu. Vynucení vlastnosti, aby byla nullable, proto ohrozit model.

Vlastnost může být ponechána bez null, přičemž pouze záložní pole je nullable. Pří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. Pří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í s výchozími nastaveními vygenerovanými úložištěm. Vzhledem k tomu, že výchozí hodnota bool CLR je "false", znamená to, že "false" nelze explicitně vložit pomocí normálního vzoru. Představte si User například typ entity:

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