Sdílet prostřednictvím


Explicitní sledování entit

Každá DbContext instance sleduje změny provedené v entitách. Když je SaveChanges zavolána, tyto sledované entity řídí změny databáze.

Sledování změn Entity Framework Core (EF Core) funguje nejlépe, když se stejná DbContext instance používá k dotazování na entity a jejich aktualizaci voláním SaveChanges. Důvodem je to, že EF Core automaticky sleduje stav dotazovaných entit a při zavolání SaveChanges zjistí všechny změny těchto entit. Tento přístup je popsaný ve službě Change Tracking v EF Core.

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.

Návod

Pro zjednodušení tento dokument používá a odkazuje na synchronní metody, jako je SaveChanges místo jejich asynchronních ekvivalentů, jako je SaveChangesAsync. Volání a čekání na asynchronní metodu lze nahradit, pokud není uvedeno jinak.

Úvod

Entity lze explicitně "připojit" k DbContext, aby kontext pak tyto entity sledoval. To je primárně užitečné v těchto případech:

  1. Vytváření nových entit, které budou vloženy do databáze.
  2. Opětovné připojení odpojených entit, které byly dříve dotazovány jinou instancí DbContext.

První z nich bude potřebovat většina aplikací a je primárně zpracována metodami DbContext.Add .

Druhý je potřeba pouze pro aplikace, které mění entity nebo jejich vztahy, zatímco entity nejsou sledovány. Webová aplikace může například odesílat entity webovému klientovi, kde uživatel provádí změny a odesílá entity zpět. Tyto entity se označují jako "odpojené", protože byly původně dotazovány z DbContext, ale při odeslání klientovi byly od tohoto kontextu odpojeny.

Webová aplikace teď musí tyto entity znovu připojit, aby byly znovu sledovány a indikovaly změny, které byly provedeny tak, aby SaveChanges mohly provádět příslušné aktualizace databáze. To je primárně zpracováváno metodami DbContext.Attach a DbContext.Update metodami.

Návod

Připojení entit ke stejné instanci DbContext , ze které byly dotazovány, by nemělo být normálně potřeba. Neprovádějte rutinně dotaz bez sledování a pak připojte vrácené entity ke stejnému kontextu. To bude pomalejší než použití sledovacího dotazu a může také vést k problémům, jako jsou chybějící hodnoty stínových vlastností, což znesnadňuje správné řešení.

Vygenerované a explicitní hodnoty klíče

Ve výchozím nastavení jsou celočíselné a guid vlastnosti klíče nakonfigurované tak, aby používaly automaticky generované hodnoty klíče. To má velkou výhodu pro sledování změn: hodnota klíče bez nastavení označuje, že entita je "nová". "Nový" znamená, že ještě nebyl vložen do databáze.

V následujících částech se používají dva modely. První je nakonfigurovaná tak, aby nepoužíla vygenerované hodnoty klíče:

public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    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; }
}

Negenerované hodnoty klíče (tj. explicitně nastavené) se v každém příkladu zobrazují jako první, protože vše je velmi explicitní a snadno se postupuje. Potom následuje příklad, ve kterém se používají vygenerované hodnoty klíče:

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

Všimněte si, že klíčové vlastnosti v tomto modelu nepotřebují žádnou další konfiguraci, protože použití vygenerovaných hodnot klíčů je výchozí pro jednoduché celočíselné klíče.

Vkládání nových entit

Explicitní hodnoty klíče

Entita musí být Added sledována ve stavu, aby byla vložena pomocí SaveChanges. Entity jsou obvykle vloženy do přidaného stavu voláním jednoho z DbContext.Add, DbContext.AddRange, DbContext.AddAsyncDbContext.AddRangeAsyncnebo ekvivalentní metody v DbSet<TEntity>.

Návod

Všechny tyto metody fungují stejným způsobem v kontextu sledování změn. Další informace najdete v tématu Další funkce sledování změn .

Pokud chcete například začít sledovat nový blog:

context.Add(
    new Blog { Id = 1, Name = ".NET Blog", });

Kontrola zobrazení ladění sledování změn po tomto volání ukazuje, že kontext sleduje novou entitu Added ve stavu:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Metody Add ale nepracují jenom na jednotlivé entitě. Ve skutečnosti začnou sledovat celý graf souvisejících entit a umístí je do Added stavu. Pokud například chcete vložit nový blog a přidružené nové příspěvky:

context.Add(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Kontext teď sleduje všechny tyto entity takto Added:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
  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} Added
  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}

Všimněte si, že explicitní hodnoty byly nastaveny pro Id klíčové vlastnosti v příkladech výše. Důvodem je to, že zde vytvořený model je nakonfigurovaný tak, aby používal explicitně nastavené hodnoty klíče, a ne automaticky vygenerované hodnoty klíče. Pokud nepoužíváte vygenerované klíče, musí být vlastnosti klíče explicitně nastaveny před voláním Add. Tyto hodnoty klíče se pak vloží při zavolání SaveChanges. Například při použití SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);

Všechny tyto entity jsou po dokončení SaveChanges sledovány ve Unchanged stavu, protože tyto entity nyní existují v databázi:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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}

Vygenerované hodnoty klíče

Jak je uvedeno výše, celočíselné a GUID vlastnosti klíče jsou nakonfigurované tak, aby ve výchozím nastavení používaly automaticky generované hodnoty klíče . To znamená, že aplikace nesmí explicitně nastavit žádnou hodnotu klíče. Pokud chcete například vložit nový blog a příspěvky, všechny s vygenerovanými hodnotami klíče:

context.Add(
    new Blog
    {
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                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
            {
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Stejně jako u explicitních hodnot klíčů teď kontext sleduje všechny tyto entity jako Added:

Blog {Id: -2147482644} Added
  Id: -2147482644 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
  Id: -2147482637 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: -2147482644}

Všimněte si v tomto případě, že se pro každou entitu vygenerovaly dočasné hodnoty klíče . Tyto hodnoty používá EF Core, dokud se nevolá SaveChanges, a v tomto okamžiku se skutečné hodnoty klíče načtou z databáze. Například při použití SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], 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();

Po dokončení funkce SaveChanges byly všechny entity aktualizovány skutečnými hodnotami klíče a jsou sledovány ve Unchanged stavu, protože teď odpovídají stavu v databázi:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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}

Jedná se o úplně stejný koncový stav jako v předchozím příkladu, který používal explicitní hodnoty klíče.

Návod

Explicitní hodnota klíče je stále možné nastavit i při použití vygenerovaných hodnot klíče. EF Core se pak pokusí vložit pomocí této klíčové hodnoty. Některé konfigurace databází, včetně SQL Serveru s identitními sloupci, nepodporují takové vkládání a vyvolají chybu (alternativní řešení najdete v těchto dokumentech).

Připojení existujících entit

Explicitní hodnoty klíče

Entity vrácené z dotazů se sledují ve stavu Unchanged. Stav Unchanged znamená, že entita nebyla změněna od doby, kdy byla dotazována. Odpojenou entitu, například vrácenou z webového klienta v požadavku HTTP, může být do tohoto stavu vložena pomocí DbContext.Attach, DbContext.AttachRangenebo ekvivalentních metod v DbSet<TEntity>. Pokud chcete například začít sledovat existující blog:

context.Attach(
    new Blog { Id = 1, Name = ".NET Blog", });

Poznámka:

Zde uvedené příklady vytvářejí entity explicitně pro jednoduchost s new. Za normálních okolností budou instance entit pocházet z jiného zdroje, jako je deserializace z klienta nebo vytváření z dat v HTTP Post.

Kontrola režimu ladění sledovače změn po tomto volání ukazuje, že entita je sledována ve stavu Unchanged.

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Stejně jako Add, Attach ve skutečnosti nastavuje celý graf propojených entit do stavu Unchanged. Pokud chcete například připojit existující blog a přidružené existující příspěvky:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Kontext teď sleduje všechny tyto entity takto Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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}

Volání SaveChanges v tomto okamžiku nebude mít žádný vliv. Všechny entity jsou označené jako Unchanged, takže v databázi není nic aktualizovat.

Vygenerované hodnoty klíče

Jak je uvedeno výše, celočíselné a GUID vlastnosti klíče jsou nakonfigurované tak, aby ve výchozím nastavení používaly automaticky generované hodnoty klíče . To má velkou výhodu při práci s odpojenými entitami: nenastavená hodnota klíče označuje, že entita ještě nebyla vložena do databáze. To umožňuje sledování změn automaticky rozpoznat nové entity a umístit je do Added stavu. Zvažte například připojení tohoto grafu blogu a příspěvků:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Blog má hodnotu klíče 1, což znamená, že již v databázi existuje. Dva z příspěvků mají nastavené klíčové hodnoty, ale třetí ne. EF Core uvidí tuto hodnotu klíče jako 0, což je výchozí hodnota CLR pro celé číslo. Výsledkem je označení nové entity ef Core jako Added místo Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  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...'

Volání SaveChanges v tomto okamžiku nic nedělá s entitami typu Unchanged, ale vloží novou entitu do databáze. Například při použití SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], 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();

Tady je důležité si uvědomit, že ef Core s vygenerovanými hodnotami klíčů dokáže automaticky odlišit nové od existujících entit v odpojeném grafu. Ve zkratce, když použijete vygenerované klíče, EF Core vždy vloží entitu, pokud tato entita nemá nastavenou žádnou hodnotu klíče.

Aktualizace existujících entit

Explicitní hodnoty klíče

DbContext.Update, DbContext.UpdateRange a ekvivalentní metody na DbSet<TEntity> se chovají přesně jako metody Attach popsané výše, s tím rozdílem, že entity se vkládají do stavu Modified místo stavu Unchanged. Pokud chcete například začít sledovat existující blog jako Modified:

context.Update(
    new Blog { Id = 1, Name = ".NET Blog", });

Kontrola zobrazení ladění sledování změn po tomto volání ukazuje, že kontext sleduje tuto entitu Modified ve stavu:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: []

Stejně jako u Add a Attachve Update skutečnosti označuje celý graf souvisejících entit jako Modified. Pokud chcete například připojit existující blog a přidružené existující příspěvky jako Modified:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Kontext teď sleduje všechny tyto entity takto Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Volání SaveChanges v tomto okamžiku způsobí odeslání aktualizací do databáze pro všechny tyto entity. Například při použití SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

Vygenerované hodnoty klíče

Stejně jako v případě Attach, vygenerované hodnoty klíče mají stejnou hlavní výhodu pro Update: nenastavená hodnota klíče označuje, že entita je nová a ještě nebyla vložena do databáze. Stejně jako v případě Attach, to umožňuje DbContext automaticky rozpoznat nové entity a dát je do Added stavu. Zvažte například volání Update pomocí tohoto grafu blogu a příspěvků:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 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,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Stejně jako v příkladu Attach se příspěvek bez hodnoty klíče zjistí jako nový a nastaví se na Added stav. Ostatní entity jsou označené jako Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
  Id: -2147482633 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Volání SaveChanges v tomto okamžiku způsobí, že se aktualizace odešlou do databáze pro všechny existující entity a vloží se nová entita. Například při použití SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], 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();

Jedná se o velmi snadný způsob generování aktualizací a vkládání z odpojeného grafu. Výsledkem však jsou aktualizace nebo vložení odesílané do databáze pro každou vlastnost každé sledované entity, i když některé hodnoty vlastností nebyly změněny. Nebudějte tím příliš vyděšený; pro mnoho aplikací s malými grafy to může být snadný a praktický způsob generování aktualizací. To znamená, že jiné složitější vzory můžou někdy vést k efektivnějším aktualizacím, jak je popsáno v řešení identity v EF Core.

Odstranění existujících entit

Aby mohla být entita smazána pomocí SaveChanges, musí být sledována ve stavu Deleted. Entity jsou obvykle vloženy do Deleted stavu voláním jednoho z DbContext.Remove, DbContext.RemoveRangenebo ekvivalentní metody on DbSet<TEntity>. Pokud chcete například označit existující příspěvek jako Deleted:

context.Remove(
    new Post { Id = 2 });

Po tomto volání se při kontrole zobrazení ladění sledování změn ukazuje, že kontext sleduje entitu ve stavu Deleted.

Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: <null> FK
  Content: <null>
  Title: <null>
  Blog: <null>

Tato entita bude odstraněna při zavolání SaveChanges. Například při použití SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Po dokončení saveChanges je odstraněná entita odpojena od DbContext, protože již v databázi neexistuje. Zobrazení ladění je proto prázdné, protože žádné entity nejsou sledovány.

Odstranění závislých nebo podřízených entit

Odstranění závislých nebo podřízených entit z grafu je snazší než odstranění nadřazených entit. Další informace najdete v následující části a Změny cizích klíčů a navigací.

Je neobvyklé volat Remove na entitu vytvořenou pomocí new. Navíc, na rozdíl od Add, Attach a Update, je neobvyklé volat Remove na entitu, která ještě není sledována ve stavu Unchanged nebo Modified. Místo toho je typické sledovat jednu entitu nebo graf souvisejících entit a poté zavolat Remove na entity k odstranění. Tento graf sledovaných entit se obvykle vytváří takto:

  1. Spuštění dotazu pro entity
  2. Použití metod Attach nebo Update v grafu odpojených entit, jak je popsáno v předchozích částech.

Například kód v předchozí části pravděpodobně získá příspěvek od klienta a poté provede něco následujícího:

context.Attach(post);
context.Remove(post);

Chová se přesně stejně jako v předchozím příkladu, protože volání Remove na nesledované entitě způsobí, že se nejprve připojí a pak označí jako Deleted.

V realističtějších příkladech se nejprve připojí graf entit a některé z těchto entit se označí jako odstraněné. Například:

// Attach a blog and associated posts
context.Attach(blog);

// Mark one post as Deleted
context.Remove(blog.Posts[1]);

Všechny entity jsou označené jako Unchanged, s výjimkou těch, u kterých Remove bylo voláno:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {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}
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}

Tato entita bude odstraněna při zavolání SaveChanges. Například při použití SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Po dokončení saveChanges je odstraněná entita odpojena od DbContext, protože již v databázi neexistuje. Ostatní entity zůstávají ve Unchanged stavu:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{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}

Odstranění hlavních nebo nadřazených entit

Každá relace spojující dva typy entit má primární nebo nadřazený konec a závislý nebo podřízený konec. Závislá/podřízená entita je entita s cizím klíčem. V relaci typu jedna-k-několika je hlavní/rodičovský objekt na straně jedna a závislý/dětský objekt na straně několika. Další informace najdete v tématu Vztahy.

V předchozích příkladech jsme mazali příspěvek, což je závislá/podřízená entita v relaci jeden ku mnoha příspěvků na blogu. To je poměrně jednoduché, protože odebrání závislé/podřízené entity nemá žádný vliv na jiné entity. Na druhou stranu odstranění hlavní nadřazené entity musí také mít vliv na všechny závislé/podřízené entity. Pokud tak neučiníte, zanecháte hodnotu cizího klíče, která odkazuje na hodnotu primárního klíče, jež již neexistuje. Jedná se o neplatný stav modelu a ve většině databází dojde k chybě referenčního omezení.

Tento neplatný stav modelu lze zpracovat dvěma způsoby:

  1. Nastavení hodnot FK na hodnotu null To znamená, že závislé osoby/děti už nejsou spojeny s žádným nositelem/rodičem. Toto je výchozí hodnota pro volitelné relace, ve kterých musí být cizí klíč null. Nastavení klíče FK na hodnotu null není platné pro požadované relace, kde cizí klíč je obvykle nenulový.
  2. Odstranění závislých osob/dětí Toto je výchozí hodnota pro požadované relace a je také platná pro volitelné relace.

Podrobné informace o sledování změn a relacích najdete v tématu Změna cizích klíčů a navigace .

Volitelné relace

Vlastnost cizího Post.BlogId klíče je v modelu, který jsme používali, null. To znamená, že relace je volitelná, a proto výchozí chování EF Core je nastavit BlogId vlastnosti cizího klíče na hodnotu null při odstranění blogu. Například:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Kontrola zobrazení ladění sledování změn po volání Remove ukazuje, že podle očekávání je blog nyní označen jako Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Zajímavější je, že všechny související příspěvky jsou nyní označeny jako Modified. Důvodem je to, že vlastnost cizího klíče v každé entitě byla nastavena na hodnotu null. Volání SaveChanges aktualizuje hodnotu cizího klíče pro každý příspěvek na hodnotu null v databázi před odstraněním blogu:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();

Po dokončení saveChanges je odstraněná entita odpojena od DbContext, protože již v databázi neexistuje. Ostatní entity jsou teď označené jako Unchanged s hodnotami cizího klíče null, které odpovídají stavu databáze.

Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: <null> FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: <null> FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Požadované relace

Pokud je vlastnost cizího klíče Post.BlogId nenulovatelná, stane se vztah mezi blogy a příspěvky povinný. V takovém případě EF Core ve výchozím nastavení odstraní podřízené/závislé entity, když je odstraněn nadřazený objekt. Například odstranění blogu se souvisejícími příspěvky jako v předchozím příkladu:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Kontrola debug zobrazení sledovače změn po volání Remove ukazuje, že podle očekávání byl blog znovu označen jako Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
  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}

Zajímavější v tomto případě je, že všechny související příspěvky byly také označeny jako Deleted. Volání funkce SaveChanges způsobí odstranění blogu a všech souvisejících příspěvků z databáze:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
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=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;

Po dokončení funkce SaveChanges se všechny odstraněné entity odpojí od DbContext, protože už v databázi neexistují. Výstup z ladícího zobrazení je proto prázdný.

Poznámka:

Tento dokument se jenom dotýká povrchu ve své práci s relacemi v EF Core. Další informace o relacích modelování a změně cizích klíčů a navigace najdete v tématu Relace, kde najdete další informace o aktualizaci nebo odstranění závislých/podřízených entit při volání SaveChanges.

Vlastní sledování s využitím TrackGraphu

ChangeTracker.TrackGraph funguje jako Add, Attach a Update s tím rozdílem, že generuje zpětné volání pro každou instanci entity před sledováním. To umožňuje použít vlastní logiku při určování způsobu sledování jednotlivých entit v grafu.

Představte si například pravidlo, které EF Core používá při sledování entit s vygenerovanými hodnotami klíče: pokud je hodnota klíče nula, pak je entita nová a měla by být vložena. Pojďme toto pravidlo rozšířit tak, aby řeklo, jestli je hodnota klíče záporná, pak by se měla entita odstranit. To nám umožňuje změnit hodnoty primárního klíče v entitách odpojeného grafu tak, aby označovaly odstraněné entity:

blog.Posts.Add(
    new Post
    {
        Title = "Announcing .NET 5.0",
        Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
    }
);

var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;

Tento odpojený graf se pak dá sledovat pomocí TrackGraphu:

public static async Task UpdateBlog(Blog blog)
{
    using var context = new BlogsContext();

    context.ChangeTracker.TrackGraph(
        blog, node =>
        {
            var propertyEntry = node.Entry.Property("Id");
            var keyValue = (int)propertyEntry.CurrentValue;

            if (keyValue == 0)
            {
                node.Entry.State = EntityState.Added;
            }
            else if (keyValue < 0)
            {
                propertyEntry.CurrentValue = -keyValue;
                node.Entry.State = EntityState.Deleted;
            }
            else
            {
                node.Entry.State = EntityState.Modified;
            }

            Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
        });

    await context.SaveChangesAsync();
}

Pro každou entitu v grafu kód výše zkontroluje hodnotu primárního klíče před sledováním entity. Pro nenastavené (nula) hodnoty klíče kód postupuje podle normálního chování EF Core. To znamená, že pokud klíč není nastavený, je entita označena jako Added. Pokud je klíč nastaven a hodnota není záporná, je entita označena jako Modified. Pokud se však najde záporná hodnota klíče, obnoví se její skutečná nezáporná hodnota a entita se bude sledovat jako Deleted.

Výstupem spuštění tohoto kódu je:

Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added

Poznámka:

Pro zjednodušení tento kód předpokládá, že každá entita má celočíselnou vlastnost primárního klíče volanou Id. To může být kodifikováno do abstraktní základní třídy nebo rozhraní. Případně lze z metadat získat IEntityType vlastnost nebo vlastnosti primárního klíče, aby tento kód fungoval s libovolným typem entity.

TrackGraph má dvě přetížení. V jednoduchém přetížení použitém výše EF Core určuje, kdy má přestat procházet graf. Konkrétně přestane navštěvovat nové související entity z dané entity, pokud je tato entita již sledována, nebo když zpětné volání nezačne sledovat entitu.

Pokročilé přetížení ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>) má zpětné volání, které vrací boolean hodnotu. Pokud zpětné volání vrátí hodnotu false, procházení grafu se zastaví, jinak bude pokračovat. Při použití tohoto přetížení je třeba dbát na to, aby nedocházelo k nekonečným smyčkám.

Rozšířené přetížení také umožňuje dodání stavu do TrackGraph a tento stav se pak předá každému zpětnému volání.