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 Post
d, 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 Post
yang terkaitnya Blog
telah 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 Blog
Post
. Post
s 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.