Globální filtry dotazů

Globální filtry dotazů jsou predikáty dotazu LINQ použité u typů entit v modelu metadat (obvykle v OnModelCreating). Predikát dotazu je logický výraz, který se obvykle předává operátoru dotazu LINQ Where . EF Core použije tyto filtry automaticky na všechny dotazy LINQ zahrnující tyto typy entit. EF Core je také aplikuje na typy entit, na které se odkazuje nepřímo prostřednictvím vlastnosti Include nebo navigation. Mezi běžné způsoby použití této funkce patří:

  • Obnovitelné odstranění – Typ entity definuje IsDeleted vlastnost.
  • Víceklientská architektura – Typ entity definuje TenantId vlastnost.

Příklad

Následující příklad ukazuje, jak pomocí globálních filtrů dotazů implementovat chování dotazů s více tenanty a obnovitelné odstranění v jednoduchém modelu blogování.

Tip

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

Poznámka

Víceklientská architektura se zde používá jako jednoduchý příklad. K dispozici je také článek s komplexními pokyny pro víceklientské architektury v aplikacích EF Core.

Nejprve definujte entity:

public class Blog
{
#pragma warning disable IDE0051, CS0169 // Remove unused private members
    private string _tenantId;
#pragma warning restore IDE0051, CS0169 // Remove unused private members

    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public bool IsDeleted { get; set; }

    public Blog Blog { get; set; }
}

Poznamenejte si deklaraci _tenantId pole v entitě Blog . Toto pole se použije k přidružení každé instance blogu ke konkrétnímu tenantovi. Definuje se IsDeleted také vlastnost Post typu entity. Tato vlastnost se používá ke sledování toho, jestli byla instance příspěvku "obnovitelně odstraněna". To znamená, že instance je označena jako odstraněná bez fyzického odebrání podkladových dat.

Dále nakonfigurujte filtry dotazů pomocí OnModelCreatingHasQueryFilter rozhraní API.

modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);

Predikátové výrazy předané HasQueryFilter volání se teď automaticky použijí na všechny dotazy LINQ pro tyto typy.

Tip

Všimněte si použití pole na úrovni instance DbContext: _tenantId slouží k nastavení aktuálního tenanta. Filtry na úrovni modelu budou používat hodnotu ze správné instance kontextu (to znamená instance, která spouští dotaz).

Poznámka

V současné době není možné definovat více filtrů dotazu na stejnou entitu – použije se pouze poslední filtr. Pomocí logického AND operátoru&& (v jazyce C#) ale můžete definovat jeden filtr s více podmínkami.

Použití navigace

Navigace můžete použít také při definování globálních filtrů dotazů. Použití navigace ve filtru dotazu způsobí, že se filtry dotazů použijí rekurzivně. Když EF Core rozšíří navigace použité ve filtrech dotazů, použije také filtry dotazů definované u odkazovaných entit.

Chcete-li znázornit tuto konfiguraci filtrů OnModelCreating dotazů následujícím způsobem:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog);
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Posts.Count > 0);
modelBuilder.Entity<Post>().HasQueryFilter(p => p.Title.Contains("fish"));

V dalším kroku zadejte dotaz na všechny Blog entity:

var filteredBlogs = db.Blogs.ToList();

Tento dotaz vytvoří následující SQL, který použije filtry dotazů definované pro obě Blog i Post entity:

SELECT [b].[BlogId], [b].[Name], [b].[Url]
FROM [Blogs] AS [b]
WHERE (
    SELECT COUNT(*)
    FROM [Posts] AS [p]
    WHERE ([p].[Title] LIKE N'%fish%') AND ([b].[BlogId] = [p].[BlogId])) > 0

Poznámka

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 je zadán nesprávně, cykly můžou během překladu dotazů vést k nekonečné smyčce.

Přístup k entitě s filtrem dotazů pomocí 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 očekává, že související entita bude vždy k dispozici. Pokud je potřeba související entita filtrem dotazu vyfiltrovat, nezpůsobí to ani nadřazená entita. Proto můžete ve výsledku získat méně prvků, než se čekalo.

K ilustraci problému můžeme použít výše Blog uvedené entity a Post následující OnModelCreating metodu:

modelBuilder.Entity<Blog>().HasMany(b => b.Posts).WithOne(p => p.Blog).IsRequired();
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.Url.Contains("fish"));

Model lze sesedovat s následujícími daty:

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/fish",
        Posts = new List<Post>
        {
            new Post { Title = "Fish care 101" },
            new Post { Title = "Caring for tropical fish" },
            new Post { Title = "Types of ornamental fish" }
        }
    });

db.Blogs.Add(
    new Blog
    {
        Url = "http://sample.com/blogs/cats",
        Posts = new List<Post>
        {
            new Post { Title = "Cat care 101" },
            new Post { Title = "Caring for tropical cats" },
            new Post { Title = "Types of ornamental cats" }
        }
    });

Problém lze pozorovat při provádění dvou dotazů:

var allPosts = db.Posts.ToList();
var allPostsWithBlogsIncluded = db.Posts.Include(p => p.Blog).ToList();

Při výše uvedeném nastavení vrátí první dotaz všech 6 Posts, 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]

INNER JOIN Použijte filtry všech Posts, jejichž související Blogobjekty byly odebrány globálním filtrem dotazů.

Místo toho je možné ji vyřešit pomocí volitelné navigace. Tímto způsobem zůstane první dotaz stejný jako předtím, ale druhý dotaz teď vygeneruje LEFT JOIN a vrátí 6 výsledků.

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 určit konzistentní filtry pro obě Blog i Post entity. Tímto způsobem se na obě Blog a v obou Postpřípadech použijí odpovídající filtry . Posts, který by mohl skončit v neočekávaném stavu, se odeberou 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"));

Zakázání filtrů

Filtry mohou být zakázány pro jednotlivé dotazy LINQ pomocí operátoru IgnoreQueryFilters .

blogs = db.Blogs
    .Include(b => b.Posts)
    .IgnoreQueryFilters()
    .ToList();

Omezení

Globální filtry dotazů mají následující omezení:

  • Filtry lze definovat pouze pro kořenový typ entity hierarchie dědičnosti.