Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Globální filtry dotazů umožňují připojit filtr k typu entity a použít tento filtr při každém spuštění dotazu na daný typ entity; představte si je jako další operátor LINQ Where , který se přidá při každém dotazování typu entity. Tyto filtry jsou užitečné v různých případech.
Návod
Ukázku pro tento článek najdete na GitHubu.
Základní příklad – měkké odstranění
V některých scénářích je vhodnější místo odstranění řádku z databáze nastavit IsDeleted příznak pro označení řádku jako odstraněného. Tento vzor se označuje jako obnovitelné odstranění. Měkké smazání umožňuje v případě potřeby obnovit smazané řádky nebo zachovat auditní stopu, kde jsou smazané řádky stále přístupné. Globální filtry dotazů se dají použít k vyfiltrování měkce smazaných řádků ve výchozím nastavení, ale zároveň vám umožní k nim přistupovat zakázáním filtru pro konkrétní dotaz.
Abychom povolili měkké smazání, přidáme do našeho typu Blog vlastnost IsDeleted.
public class Blog
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public string Name { get; set; }
}
Teď jsme pomocí rozhraní API HasQueryFilternastavili globální filtr dotazů v:OnModelCreating
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
Teď se můžeme na Blog entity dotazovat obvyklým způsobem. Nakonfigurovaný filtr zajistí, že všechny dotazy standardně vyfiltrují všechny instance, kde je IsDeleted pravda.
Všimněte si, že v tomto okamžiku je nutné ručně nastavit IsDeleted, aby bylo možné entitu měkce odstranit. V případě komplexnějšího řešení můžete přepsat metodu SaveChangesAsync vašeho typu kontextu a přidat logiku, která projde všemi entitami, které uživatel odstranil, a místo toho je označí jako upravené, přičemž nastaví vlastnost IsDeleted na true.
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ChangeTracker.DetectChanges();
foreach (var item in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
{
item.State = EntityState.Modified;
item.CurrentValues["IsDeleted"] = true;
}
return await base.SaveChangesAsync(cancellationToken);
}
To vám umožní používat rozhraní EF API, která obvyklým způsobem odstraňují instanci entity, avšak místo toho je mírně odstraní.
Použití kontextových dat – multi-tenantní architektura
Dalším běžným scénářem globálních filtrů dotazů je víceklientská architektura, ve které vaše aplikace ukládá data patřící různým uživatelům ve stejné tabulce. V takových případech je obvykle sloupec ID tenanta , který přidruží řádek ke konkrétnímu tenantovi a globální filtry dotazů se dají použít k automatickému filtrování řádků aktuálního tenanta. To poskytuje silnou izolaci tenantů pro vaše dotazy ve výchozím nastavení, aniž by bylo nutné myslet na filtrování tenanta pro každý jednotlivý dotaz.
Na rozdíl od logického smazání vyžaduje víceklientský systém znalost aktuálního ID tenanta; tato hodnota se obvykle určuje například při ověřování uživatele přes web. Pro účely EF musí být ID tenanta k dispozici v kontextové instanci, aby na něj globální filtr dotazů mohl odkazovat a používat ho při dotazování. Přijměme parametr tenantId v konstruktoru typu kontextu a odkažme na něj z našeho filtru:
public class MultitenancyContext(string tenantId) : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
}
}
Tím se vynutí, aby každý, kdo vytváří kontext, určil jeho přidružené ID tenanta a ve výchozím nastavení se z dotazů vracely pouze Blog entity s tímto ID.
Poznámka:
Tato ukázka ukázala pouze základní koncepty víceklientské architektury potřebné k předvedení globálních filtrů dotazů. Další informace o víceklientské architektury a EF najdete v tématu víceklientská architektura v aplikacích EF Core.
Použití více filtrů dotazů
Volání HasQueryFilter pomocí jednoduchého filtru přepíše předchozí filtr, takže více filtrů nelze definovat na stejném typu entity tímto způsobem:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// The following overwrites the previous query filter:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
Poznámka:
Tato funkce se zavádí v EF Core 10.0 (ve verzi Preview).
Aby bylo možné definovat více filtrů dotazů pro stejný typ entity, musí být pojmenované:
modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
.HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);
To umožňuje spravovat každý filtr samostatně, včetně selektivního zakázání jednoho, ale ne druhého filtru.
Zakázání filtrů
Filtry mohou být zakázány pro jednotlivé dotazy LINQ pomocí operátoru IgnoreQueryFilters :
var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();
Pokud je nakonfigurovaných více pojmenovaných filtrů, zakáže se tím všechny. Pokud chcete selektivně zakázat konkrétní filtry (počínaje EF 10), předejte seznam názvů filtrů, které se mají zakázat:
var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();
Filtry dotazů a požadované navigace
Upozornění
Použití požadované navigace pro přístup k entitě, která má definovaný globální filtr dotazů, může vést k neočekávaným výsledkům.
Požadované navigace v EF znamenají, že související entita je vždy přítomna. Vzhledem k tomu, že vnitřní spojení lze použít k načtení souvisejících entit, pokud je požadovaná související entita filtrem dotazu odfiltrována, nadřazená entita se může také vyfiltrovat. To může vést k neočekávanému získání menšího počtu prvků, než se čekalo.
Abychom mohli tento problém ilustrovat, můžeme je použít Blog a Post nakonfigurovat následujícím způsobem:
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Model lze naplnit pomocí následujících dat:
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/fish",
Posts =
[
new() { Title = "Fish care 101" },
new() { Title = "Caring for tropical fish" },
new() { Title = "Types of ornamental fish" }
]
});
db.Blogs.Add(
new Blog
{
Url = "http://sample.com/blogs/cats",
Posts =
[
new() { Title = "Cat care 101" },
new() { Title = "Caring for tropical cats" },
new() { Title = "Types of ornamental cats" }
]
});
Problém lze pozorovat při provádění následujících dvou dotazů:
var allPosts = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();
Při výše uvedeném nastavení vrátí první dotaz všech 6 Post instancí, ale druhý dotaz vrátí pouze 3. K této neshodě dochází, protože Include metoda v druhém dotazu načte související Blog entity. Vzhledem k tomu, že navigace mezi Blog a Post je povinná, EF Core používá INNER JOIN při vytváření dotazu:
SELECT [p].[PostId], [p].[BlogId], [p].[Content], [p].[IsDeleted], [p].[Title], [t].[BlogId], [t].[Name], [t].[Url]
FROM [Posts] AS [p]
INNER JOIN (
SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE [b].[Url] LIKE N'%fish%'
) AS [t] ON [p].[BlogId] = [t].[BlogId]
Použití INNER JOIN filtruje všechny řádky Post, jejichž související řádky Blog byly filtrovány filtrem dotazu. Tento problém lze vyřešit konfigurací navigace jako volitelné navigace místo nucené, což způsobí, že EF vygeneruje LEFT JOIN místo INNER JOIN.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired(false);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
Alternativním přístupem je zadat konzistentní filtry pro oba entity typy Blog a Post; jakmile se odpovídající filtry použijí na obě entity Blog a Post, Post řádky, které mohou končit v neočekávaném stavu, se odstraní a oba dotazy vrátí 3 výsledky.
modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Blog.Url.Contains("fish"));
Filtry databázových dotazů a IEntityTypeConfiguration
Pokud váš filtr dotazu potřebuje získat přístup k ID tenanta nebo podobným kontextovým informacím, IEntityTypeConfiguration<TEntity> může představovat další komplikaci, na rozdíl od OnModelCreatingtoho, že neexistuje žádná instance vašeho typu kontextu, na které by bylo možné snadno odkazovat z filtru dotazu. Jako alternativní řešení přidejte do svého typu konfigurace fiktivní kontext a odkažte na něj následovně:
private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
private readonly SomeDbContext _context = null!;
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
}
}
Omezení
Globální filtry dotazů mají následující omezení:
- Filtry lze definovat pouze pro typ kořenové entity hierarchie dědičnosti.
- V současné době EF Core nerozpozná cykly v definicích globálního filtru dotazů, proto byste při jejich definování měli být opatrní. Pokud jsou zadány nesprávně, cykly mohou během překladu dotazů vést k nekonečným smyčkám.