Filter Kueri Global

Filter kueri global adalah predikat kueri LINQ yang diterapkan ke Jenis Entitas dalam model metadata (biasanya dalam OnModelCreating). Predikat kueri adalah ekspresi boolean yang biasanya diteruskan ke operator kueri LINQ Where . EF Core menerapkan filter tersebut secara otomatis ke kueri LINQ apa pun yang melibatkan Jenis Entitas tersebut. EF Core juga menerapkannya ke Jenis Entitas, yang dirujuk secara tidak langsung melalui penggunaan properti Sertakan atau navigasi. Beberapa aplikasi umum dari fitur ini adalah:

  • Penghapusan sementara - Jenis Entitas menentukan IsDeleted properti.
  • Multi-penyewaan - Jenis Entitas mendefinisikan TenantId properti.

Contoh

Contoh berikut menunjukkan cara menggunakan Filter Kueri Global untuk menerapkan perilaku kueri multi-penyewa dan penghapusan sementara dalam model blogging sederhana.

Tip

Anda dapat melihat contoh artikel ini di GitHub.

Catatan

Multi-penyewaan digunakan di sini sebagai contoh sederhana. Ada juga artikel dengan panduan komprehensif untuk multi-penyewaan dalam aplikasi EF Core.

Pertama, tentukan entitas:

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

Perhatikan deklarasi _tenantId bidang pada Blog entitas. Bidang ini akan digunakan untuk mengaitkan setiap instans Blog dengan penyewa tertentu. Juga didefinisikan adalah IsDeleted properti pada Post jenis entitas. Properti ini digunakan untuk melacak apakah instans postingan telah "dihapus sementara". Artinya, instans ditandai sebagai dihapus tanpa menghapus data yang mendasar secara fisik.

Selanjutnya, konfigurasikan filter kueri dalam OnModelCreating menggunakan HasQueryFilter API.

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

Ekspresi predikat yang diteruskan ke panggilan sekarang akan secara otomatis diterapkan ke kueri LINQ apa pun untuk jenis tersebut HasQueryFilter .

Tip

Perhatikan penggunaan bidang tingkat instans DbContext: _tenantId digunakan untuk mengatur penyewa saat ini. Filter tingkat model akan menggunakan nilai dari instans konteks yang benar (yaitu, instans yang menjalankan kueri).

Catatan

Saat ini tidak dimungkinkan untuk menentukan beberapa filter kueri pada entitas yang sama - hanya yang terakhir yang akan diterapkan. Namun, Anda dapat menentukan satu filter dengan beberapa kondisi menggunakan operator logis AND (&& di C#).

Penggunaan navigasi

Anda juga bisa menggunakan navigasi dalam menentukan filter kueri global. Menggunakan navigasi dalam filter kueri akan menyebabkan filter kueri diterapkan secara rekursif. Saat EF Core memperluas navigasi yang digunakan dalam filter kueri, EF Core juga akan menerapkan filter kueri yang ditentukan pada entitas yang dirujuk.

Untuk mengilustrasikan filter OnModelCreating kueri konfigurasi ini dengan cara berikut:

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

Selanjutnya, kueri untuk semua Blog entitas:

var filteredBlogs = db.Blogs.ToList();

Kueri ini menghasilkan SQL berikut, yang menerapkan filter kueri yang ditentukan untuk Blog entitas dan Post :

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

Catatan

Saat ini EF Core tidak mendeteksi siklus dalam definisi filter kueri global, jadi Anda harus berhati-hati saat menentukannya. Jika ditentukan dengan tidak benar, siklus dapat menyebabkan perulangan tak terbatas selama terjemahan kueri.

Mengakses entitas dengan filter kueri menggunakan navigasi yang diperlukan

Perhatian

Menggunakan navigasi yang diperlukan untuk mengakses entitas yang memiliki filter kueri global yang ditentukan dapat menyebabkan hasil yang tidak terduga.

Navigasi yang diperlukan mengharapkan entitas terkait untuk selalu ada. Jika entitas terkait yang diperlukan difilter oleh filter kueri, entitas induk juga tidak akan dihasilkan. Jadi Anda mungkin mendapatkan lebih sedikit elemen dari yang diharapkan pada hasilnya.

Untuk mengilustrasikan masalah, kita dapat menggunakan Blog entitas dan Post yang ditentukan di atas dan metode berikut OnModelCreating :

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

Model dapat disemai dengan data berikut:

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

Masalah dapat diamati saat menjalankan dua kueri:

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

Dengan penyiapan di atas, kueri pertama mengembalikan semua 6 Postd, namun kueri kedua hanya mengembalikan 3. Ketidakcocokan ini terjadi karena Include metode di kueri kedua memuat entitas terkait Blog . Karena navigasi antara Blog dan Post diperlukan, EF Core menggunakan INNER JOIN saat membuat kueri:

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]

Penggunaan INNER JOIN filter semua Postyang terkaitnya Blogtelah dihapus oleh filter kueri global.

Ini dapat diatasi dengan menggunakan navigasi opsional alih-alih diperlukan. Dengan cara ini kueri pertama tetap sama seperti sebelumnya, namun kueri kedua sekarang akan menghasilkan LEFT JOIN dan mengembalikan 6 hasil.

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

Pendekatan alternatif adalah menentukan filter yang konsisten pada entitas Blog dan Post . Cara ini mencocokkan filter diterapkan ke dan BlogPost. Posts yang bisa berakhir dalam keadaan tidak terduga dihapus dan kedua kueri mengembalikan 3 hasil.

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

Menonaktifkan Filter

Filter dapat dinonaktifkan untuk kueri LINQ individual dengan menggunakan IgnoreQueryFilters operator.

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

Batasan

Filter kueri global memiliki batasan berikut:

  • Filter hanya dapat ditentukan untuk Jenis Entitas akar hierarki warisan.