Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Filter kueri global memungkinkan melampirkan filter ke jenis entitas dan menerapkan filter tersebut setiap kali kueri pada jenis entitas tersebut dijalankan; anggap saja sebagai operator LINQ Where tambahan yang ditambahkan setiap kali jenis entitas dikueri. Filter semacam itu berguna dalam berbagai kasus.
Petunjuk / Saran
Anda dapat melihat contoh artikel ini di GitHub.
Contoh dasar - penghapusan sementara
Dalam beberapa skenario, daripada menghapus baris dari database, lebih baik mengatur IsDeleted bendera untuk menandai baris sebagai dihapus; pola ini disebut penghapusan sementara. Penghapusan sementara memungkinkan baris dapat dipulihkan jika diperlukan, atau untuk mempertahankan jejak audit di mana baris yang dihapus masih dapat diakses. Filter kueri global dapat digunakan untuk memfilter baris yang dihapus sementara secara default, sambil tetap memungkinkan Anda mengaksesnya di tempat tertentu dengan menonaktifkan filter untuk kueri tertentu.
Untuk mengaktifkan penghapusan sementara, mari kita tambahkan properti IsDeleted pada tipe Blog kami.
public class Blog
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public string Name { get; set; }
}
Kami sekarang menyiapkan filter kueri global, menggunakan HasQueryFilter API di OnModelCreating:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
Kita sekarang dapat mengkueri entitas kita Blog seperti biasa; filter yang dikonfigurasi akan memastikan bahwa semua kueri akan - secara default - memfilter semua instans yang IsDeleted benar.
Perhatikan bahwa pada titik ini, Anda harus mengatur IsDeleted secara manual untuk menghapus secara lembut entitas. Untuk solusi yang lebih end-to-end, Anda dapat mengoverride metode jenis SaveChangesAsync konteks Anda untuk menambahkan logika yang melewati semua entitas yang dihapus pengguna, dan mengubahnya agar dimodifikasi sebagai alternatif, mengatur properti IsDeleted ke 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);
}
Ini memungkinkan Anda menggunakan API EF yang menghapus instans entitas seperti biasa dan membuatnya dihapus sementara sebagai gantinya.
Menggunakan data konteks - multi-penyewaan
Skenario mainstream lain untuk filter kueri global adalah multi-penyewaan, di mana aplikasi Anda menyimpan data milik pengguna yang berbeda dalam tabel yang sama. Dalam kasus seperti itu, biasanya ada kolom ID penyewa yang mengaitkan baris ke penyewa tertentu, dan filter kueri global dapat digunakan untuk memfilter baris penyewa saat ini secara otomatis. Ini menyediakan isolasi penyewa yang kuat untuk kueri Anda secara default, menghapus kebutuhan untuk memikirkan pemfilteran untuk penyewa di setiap kueri.
Tidak seperti penghapusan sementara, multi-penyewa memerlukan mengetahui ID penyewa saat ini ; nilai ini biasanya ditentukan misalnya ketika pengguna mengautentikasi melalui web. Untuk tujuan EF, ID penyewa harus tersedia pada instans konteks, sehingga filter kueri global dapat merujuknya dan menggunakannya saat mengkueri. Mari kita terima tenantId parameter dalam konstruktor jenis konteks kita, dan referensikan itu dari filter kita:
public class MultitenancyContext(string tenantId) : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
}
}
Ini memaksa siapa pun yang membuat konteks untuk menentukan ID penyewa terkait, dan memastikan bahwa hanya Blog entitas dengan ID tersebut yang dikembalikan dari kueri secara default.
Nota
Sampel ini hanya memperlihatkan konsep multipenyewa dasar yang diperlukan untuk menunjukkan filter kueri global. Untuk informasi selengkapnya tentang multi-penyewaan dan EF, lihat multi-penyewaan dalam aplikasi EF Core.
Menggunakan beberapa filter kueri
Panggilan HasQueryFilter dengan filter sederhana menimpa filter sebelumnya, sehingga beberapa filter tidak dapat ditentukan pada jenis entitas yang sama dengan cara ini:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// The following overwrites the previous query filter:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
Nota
Fitur ini sedang diperkenalkan di EF Core 10.0 (dalam pratinjau).
Untuk menentukan beberapa filter kueri pada jenis entitas yang sama, filter tersebut harus diberi nama:
modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
.HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);
Ini memungkinkan Anda mengelola setiap filter secara terpisah, termasuk menonaktifkan filter secara selektif tetapi tidak yang lain.
Menonaktifkan filter
Filter dapat dinonaktifkan untuk kueri LINQ individual dengan menggunakan IgnoreQueryFilters operator:
var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();
Jika beberapa filter bernama dikonfigurasi, ini akan menonaktifkan semuanya. Untuk menonaktifkan filter tertentu secara selektif (dimulai dengan EF 10), teruskan daftar nama filter yang akan dinonaktifkan:
var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();
Filter kueri dan 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 di EF menyiratkan bahwa entitas terkait selalu ada. Karena gabungan dalam dapat digunakan untuk mengambil entitas terkait, jika entitas terkait yang diperlukan difilter oleh filter kueri, entitas induk juga dapat difilter. Ini dapat mengakibatkan pengambilan elemen dalam jumlah yang lebih sedikit dari yang diharapkan secara tidak terduga.
Untuk mengilustrasikan masalah, kita dapat menggunakan Blog dan Post entitas dan mengonfigurasinya sebagai berikut:
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() { 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" }
]
});
Masalah dapat diamati saat menjalankan dua kueri berikut:
var allPosts = await db.Posts.ToListAsync();
var allPostsWithBlogsIncluded = await db.Posts.Include(p => p.Blog).ToListAsync();
Dengan penyiapan di atas, kueri pertama mengembalikan semua 6 Post instans, tetapi 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 menyaring semua baris Post yang baris terkaitnya telah Blog dihapus oleh filter kueri. Masalah ini dapat diatasi dengan mengonfigurasi navigasi sebagai navigasi opsional alih-alih diperlukan, menyebabkan EF menghasilkan LEFT JOIN alih-alih INNER JOIN:
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 jenis entitas Blog dan Post; setelah filter yang cocok diterapkan ke Blog dan Post, baris Post yang dapat 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"));
Filter kueri dan IEntityTypeConfiguration
Jika filter kueri Anda perlu mengakses ID penyewa atau informasi konteks serupa, IEntityTypeConfiguration<TEntity> dapat menjadi tambahan komplikasi karena, tidak seperti OnModelCreating, tidak ada instans jenis konteks Anda yang tersedia untuk dirujuk pada filter kueri. Sebagai solusinya, tambahkan konteks dummy ke jenis konfigurasi Anda dan referensikan sebagai berikut:
private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
private readonly SomeDbContext _context = null!;
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
}
}
Keterbatasan
Filter kueri global memiliki batasan berikut:
- Filter hanya dapat didefinisikan untuk jenis entitas akar hierarki pewarisan.
- 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.