Odpojené entity

Instance DbContext automaticky bude sledovat entity vrácené z databáze. Změny provedené v těchto entitách se pak zjistí při zavolání SaveChanges a databáze se podle potřeby aktualizuje. Podrobnosti najdete v tématu Základní ukládání a související data .

Někdy se ale entity dotazují pomocí jedné kontextové instance a pak se uloží pomocí jiné instance. K tomu často dochází v odpojených scénářích, jako je webová aplikace, kde se entity dotazují, odesílají klientovi, upravují, odesílají se zpět na server v požadavku a pak se uloží. V tomto případě musí druhá instance kontextu vědět, jestli jsou entity nové (měly by být vloženy) nebo existující (měly by být aktualizovány).

Návod

Ukázku pro tento článek najdete na GitHubu.

Návod

EF Core může sledovat pouze jednu instanci jakékoli entity s danou hodnotou primárního klíče. Nejlepším způsobem, jak tomu zabránit, je použití krátkodobého kontextu pro každou jednotku práce tak, aby kontext začal prázdný, obsahoval entity, uložil tyto entity a pak byl kontext odstraněn a zlikvidován.

Identifikace nových entit

Klient identifikuje nové entity.

Nejjednodušším případem řešení je, když klient informuje server, jestli je entita nová nebo existující. Často se například požadavek na vložení nové entity liší od požadavku na aktualizaci existující entity.

Zbývající část této části popisuje případy, kdy je nutné určit jiným způsobem, zda vložit nebo aktualizovat.

S automaticky generovanými klíči

Hodnotu automaticky vygenerovaného klíče lze často použít k určení, jestli je potřeba vložit nebo aktualizovat entitu. Pokud klíč nebyl nastaven (to znamená, že stále má výchozí hodnotu CLR null, nula atd.), musí být entita nová a musí ji vložit. Na druhou stranu, pokud byla hodnota klíče nastavena, musela již být dříve uložena a teď potřebuje aktualizaci. Jinými slovy, pokud má klíč hodnotu, entita byla dotazována, odeslána klientovi a nyní se vrátila k aktualizaci.

Pokud je typ entity známý, je snadné zkontrolovat nenastavený klíč.

public static bool IsItNew(Blog blog)
    => blog.BlogId == 0;

Ef ale také nabízí integrovaný způsob, jak to udělat pro libovolný typ entity a typ klíče:

public static bool IsItNew(DbContext context, object entity)
    => !context.Entry(entity).IsKeySet;

Návod

Klíče se nastaví, jakmile budou entity sledovány kontextem, i když je entita ve stavu Přidání. To pomáhá při procházení grafu entit a rozhodování o tom, co s jednotlivými entitami dělat, například při používání rozhraní API TrackGraph. Hodnota klíče by měla být použita pouze způsobem uvedeným zde před provedením jakéhokoli volání ke sledování entity.

S dalšími klíči

K identifikaci nových entit je potřeba nějaký jiný mechanismus, když se hodnoty klíče negenerují automaticky. Existují dva obecné přístupy k tomuto:

  • Dotaz na objekt
  • Předání příznaku z klienta

K dotazování na entitu stačí použít metodu Find:

public static async Task<bool> IsItNew(BloggingContext context, Blog blog)
    => (await context.Blogs.FindAsync(blog.BlogId)) == null;

Tento dokument nezahrnuje úplný kód předávání příznaku od klienta. Ve webové aplikaci to obvykle znamená provádění různých požadavků na různé akce nebo předání nějakého stavu v požadavku a následné extrakci v kontroleru.

Ukládání jednotlivých entit

Pokud víte, jestli je potřeba vložit nebo aktualizovat, můžete použít možnost Přidat nebo Aktualizovat odpovídajícím způsobem:

public static async Task Insert(DbContext context, object entity)
{
    context.Add(entity);
    await context.SaveChangesAsync();
}

public static async Task Update(DbContext context, object entity)
{
    context.Update(entity);
    await context.SaveChangesAsync();
}

Pokud ale entita používá hodnoty automaticky generovaného klíče, lze metodu Update použít v obou případech:

public static async Task InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    await context.SaveChangesAsync();
}

Metoda Update obvykle označuje entitu pro aktualizaci, nikoli vložení. Pokud má ale entita automaticky vygenerovaný klíč a nebyla nastavena žádná hodnota klíče, je entita místo toho automaticky označena pro vložení.

Pokud entita nepoužívá automaticky generované klíče, aplikace se musí rozhodnout, jestli se má entita vložit nebo aktualizovat: Například:

public static async Task InsertOrUpdate(BloggingContext context, Blog blog)
{
    var existingBlog = await context.Blogs.FindAsync(blog.BlogId);
    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
    }

    await context.SaveChangesAsync();
}

Tady jsou kroky:

  • Pokud funkce Najít vrátí hodnotu null, databáze ještě neobsahuje blog s tímto ID, takže zavoláme funkci Add, abychom ho označili pro vložení.
  • Pokud funkce Najít vrátí entitu, existuje v databázi a kontext teď sleduje existující entitu.
    • Pak pomocí SetValues nastavíme hodnoty pro všechny vlastnosti této entity na ty, které pocházejí z klienta.
    • Volání SetValues označí entitu, která se má podle potřeby aktualizovat.

Návod

SetValues označí pouze jako změněné vlastnosti, které mají jiné hodnoty než ty ve sledované entitě. To znamená, že po odeslání aktualizace budou aktualizovány pouze sloupce, které se skutečně změnily. (A pokud se nic nezměnilo, nebude se vůbec odesílat žádná aktualizace.)

Práce s grafy

Rozlišení identity

Jak jsme uvedli výše, EF Core může sledovat pouze jednu instanci jakékoli entity s danou hodnotou primárního klíče. Při práci s grafy by měl být graf ideálně vytvořen tak, aby byl tento invariant zachován a kontext by se měl použít pouze pro jednu jednotku práce. Pokud graf obsahuje duplicity, bude nutné graf před odesláním do EF zpracovat a konsolidovat více instancí do jednoho. To nemusí být triviální, pokud instance mají konfliktní hodnoty a relace, takže sloučení duplicit by se mělo v kanálu aplikace provést co nejdříve, aby se zabránilo řešení konfliktů.

Všechny nové nebo všechny existující entity

Příkladem práce s grafy je vložení nebo aktualizace blogu společně s jeho kolekcí přidružených příspěvků. Pokud by měly být vloženy všechny entity v grafu nebo všechny by měly být aktualizovány, je proces stejný, jak je popsáno výše u jednotlivých entit. Například graf blogů a příspěvků vytvořených takto:

var blog = new Blog
{
    Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};

lze vložit takto:

public static async Task InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    await context.SaveChangesAsync();
}

Volání přidat označí blog a všechny příspěvky, které se mají vložit.

Podobně platí, že pokud je potřeba aktualizovat všechny entity v grafu, můžete použít aktualizaci:

public static async Task UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    await context.SaveChangesAsync();
}

Blog a všechny jeho příspěvky budou označeny k aktualizaci.

Kombinace nových a existujících entit

S automaticky generovanými klíči je možné aktualizaci znovu použít pro vložení i aktualizace, i když graf obsahuje kombinaci entit, které vyžadují vložení, a pro ty, které vyžadují aktualizaci:

public static async Task InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    await context.SaveChangesAsync();
}

Aktualizace označí všechny entity v grafu, blogu nebo příspěvku pro vložení, pokud nemá nastavenou hodnotu klíče, zatímco všechny ostatní entity jsou označené k aktualizaci.

Stejně jako v případě, že nepoužíváte automaticky generované klíče, můžete použít dotaz a některé zpracování:

public static async Task InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = await context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }
    }

    await context.SaveChangesAsync();
}

Řízení odstraňování

Odstranění může být obtížné zpracovat, protože často absence entity znamená, že by měla být odstraněna. Jedním ze způsobů, jak to vyřešit, je použití "soft delete", takže entita je označena jako odstraněná, místo aby byla skutečně odstraněna. Odstranění se pak změní na stejné jako aktualizace. Měkké mazání je možné implementovat pomocí filtrů dotazů.

U skutečného smazání je běžným vzorem použití rozšíření vzoru dotazu pro provádění v podstatě grafového rozdílu. Například:

public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = await context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }

        foreach (var post in existingBlog.Posts)
        {
            if (!blog.Posts.Any(p => p.PostId == post.PostId))
            {
                context.Remove(post);
            }
        }
    }

    await context.SaveChangesAsync();
}

TrackGraph

Uvnitř systému, Přidat, Připojit a Aktualizovat používají procházení grafem s určením pro každou entitu, zda by měla být označena jako Přidáno (vložení), Změněno (aktualizace), Beze změny (nic nedělat) nebo Odstraněno (odstranit). Tento mechanismus je přístupný prostřednictvím rozhraní API TrackGraph. Předpokládejme například, že když klient odešle zpět graf entit, nastaví u každé entity příznak označující, jak se má zpracovat. TrackGraph pak můžete použít ke zpracování tohoto příznaku:

public static async Task SaveAnnotatedGraph(DbContext context, object rootEntity)
{
    context.ChangeTracker.TrackGraph(
        rootEntity,
        n =>
        {
            var entity = (EntityBase)n.Entry.Entity;
            n.Entry.State = entity.IsNew
                ? EntityState.Added
                : entity.IsChanged
                    ? EntityState.Modified
                    : entity.IsDeleted
                        ? EntityState.Deleted
                        : EntityState.Unchanged;
        });

    await context.SaveChangesAsync();
}

Příznaky se zobrazí pouze jako součást entity kvůli jednoduchosti příkladu. Příznaky by obvykle byly součástí DTO nebo některého jiného stavu zahrnutého v daném požadavku.