全域查詢篩選條件

全域查詢篩選準則是元資料模型中套用至實體類型的 LINQ 查詢述詞(通常是 在 OnModelCreating 中)。 查詢述詞是布林運算式,通常傳遞至 LINQ Where 查詢運算子。 EF Core 會自動將這類篩選套用至任何涉及這些實體類型的 LINQ 查詢。 EF Core 也會將它們套用至實體類型,透過使用 Include 或 navigation 屬性間接參考。 此功能的一些常見應用如下:

  • 虛刪除 - 實體類型會 IsDeleted 定義屬性。
  • 多租使用者 - 實體類型會 TenantId 定義屬性。

範例

下列範例示範如何使用全域查詢篩選,在簡單的部落格模型中實作多租使用者和虛刪除查詢行為。

提示

您可以檢視本文中的 GitHut 範例

注意

這裡使用多租使用者做為簡單的範例。 另外還有一篇文章,其中包含 EF Core 應用程式中 多租使用者的完整指引

首先,定義實體:

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

請注意實體上欄位的 _tenantIdBlog 宣告。 此欄位將用來將每個部落格實例與特定租使用者產生關聯。 此外,定義也是 IsDeleted 實體類型上的 Post 屬性。 這個屬性用來追蹤 post 實例是否已「虛刪除」。 亦即,將執行個體標示為已刪除,而不實際移除底層資料。

接下來,使用 HasQueryFilter API 在 中 OnModelCreating 設定查詢篩選。

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

傳遞給呼叫的 HasQueryFilter 述詞運算式現在會自動套用至這些類型的任何 LINQ 查詢。

提示

請注意 DbContext 執行個體層級欄位的使用方式:_tenantId 用來設定目前的租用戶。 模型層級篩選將會使用正確內容執行個體 (亦即,執行查詢的執行個體) 中的值。

注意

目前無法在同一個實體上定義多個查詢篩選準則, 只會套用最後一個查詢篩選準則。 不過,您可以使用邏輯 AND 運算子來定義具有多個條件的單一篩選準則( && 在 C# 中)。

使用導覽

您也可以在定義全域查詢篩選準則時使用導覽。 在查詢篩選中使用導覽會導致以遞迴方式套用查詢篩選。 當 EF Core 展開查詢篩選中使用的導覽時,它也會套用在參考實體上定義的查詢篩選。

若要以下列方式說明此設定查詢篩選 OnModelCreating

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

接下來,查詢所有 Blog 實體:

var filteredBlogs = db.Blogs.ToList();

此查詢會產生下列 SQL,其會套用針對 BlogPost 實體定義的查詢篩選:

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

注意

目前 EF Core 不會偵測全域查詢篩選定義的迴圈,因此在定義它們時應該小心。 如果指定不正確,迴圈可能會導致查詢轉譯期間產生無限迴圈。

使用必要的導覽來存取具有查詢篩選的實體

警告

使用必要的導覽來存取已定義全域查詢篩選的實體,可能會導致非預期的結果。

必要的導覽預期相關實體一律存在。 如果查詢篩選準則篩選出必要的相關實體,父實體也不會產生結果。 因此,您可能會得到比預期少的專案結果。

為了說明問題,我們可以使用 Blog 上述和下列 OnModelCreating 方法指定的 和 Post 實體:

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

此模型可以植入下列資料:

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

執行兩個查詢時,可以觀察到此問題:

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

在上述設定中,第一個查詢會傳回所有 6 Post 秒,但第二個查詢只會傳回 3。 因為第二個查詢中的 方法會載入相關的 Blog 實體,因此會發生這種不相符的情況 Include 。 由於 需要 和 Post 之間的 Blog 流覽,EF Core 會在建構查詢時使用 INNER JOIN

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使用篩選會排除全域查詢篩選已移除其相關 Blog 的所有 Post 篩選。

您可以使用選擇性導覽而非必要方式加以定址。 如此一來,第一個查詢會保持不變,不過第二個查詢現在 LEFT JOIN 會產生並傳回 6 個結果。

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

替代方法是在 和 Post 實體上 Blog 指定一致的篩選。 如此一來,比對篩選會同時套用至 BlogPostPost會移除最終處於非預期狀態的 s,且這兩個查詢都會傳回 3 個結果。

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

停用篩選條件

可能會使用 IgnoreQueryFilters 運算子來停用個別 LINQ 查詢的篩選條件。

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

限制

全域查詢篩選條件具有下列限制:

  • 只能針對繼承階層的根實體類型定義篩選條件。