前言
EntityFramework Core每一次版本的迭代和更新都會帶給我們驚喜,每次都會盡量滿足大部分使用者的需求。在EF Core 2.0版本中出現了全局過濾新特性即HasQueryFilter,它出現的意義在哪里?能夠解決什么問題呢?這是我們需要思考的問題。通過HasQueryFilter方法來創建過濾器能夠允許我們對訪問特定數據庫表的所有查詢額外添加一模一樣的過濾器。它主要用於軟刪除(soft-delete)場景,即用戶並不想返回那些被標記為已刪除但是尚未從數據庫中做物理刪除的數據行。
HasQueryFilter全局過濾器
在文章開頭我們講述了HasQueryFilter特性所解決的問題,下面我們利用實際例子來講述一下。在許多場景中我們並不會從物理上刪除數據,而只是僅僅改變數據的狀態。這個時候就凸顯了HasQueryFilter特性的作用。比如我們創建一個博客實體。
public class Blog { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public bool IsDeleted { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public ICollection<Post> Posts { get; set; } }
是不是因為我們本節講述全局過濾所以才會加IsDeleted字段呢?顯然不是,這個是有其應用場景,當我們管理自己的博客時,可能瞌睡沒睡好(當然也有其他原因,不必糾結於此)導致一個不小心刪除了某一篇文章即使在博客后台有友好提示【是否刪除該博客】,但是你依然手滑刪除了,這個時候我們找到博客園運營反應,然后分分鍾就還原了我們所刪除的博客,就像將文件扔到了回收站再還原一樣。接下來我們在OnModelCreating方法或者單獨建立的映射類中配置過濾未被刪除的博客,如下:
modelBuilder.Entity<Blog>(e => { e.HasQueryFilter(f => !f.IsDeleted); }); 或者 public class BlogConfiguration : IEntityTypeConfiguration<Blog> { public void Configure(EntityTypeBuilder<Blog> builder) { builder.HasQueryFilter(f => !f.IsDeleted); ...... } }
要是我們對於特殊場景需要禁用查詢過濾或者說忽略查詢過濾又該如何呢?通過對應的APi(IgnoreQueryFilters)即可,如下:
using (var context = new EFCoreDbContext()) { var blogs = context.Blogs .Include(d => d.Posts) .IgnoreQueryFilters() .AsNoTracking() .ToList(); }
使用HasQueryFilter進行查詢過濾是不是就是如此簡單呢?是不是問題到此就這樣結束了呢?看過Jeff博客的童鞋知道,Jeff經常問這樣的問題且自問自答,肯定不止於此,我們繼續往下探索。上述我們只是應用一個博客實體,我們還存在發表實體且二者存在關聯關系,我們同上在Post中定義IsDeleted字段,此時我們在對Blog進行過濾的同時也對Post進行過濾,如下:
builder.HasQueryFilter(f => !f.IsDeleted && f.Posts.All(w => !w.IsDeleted));
由上得知使用HasQueryFilter進行過濾篩選的局限性之一。
局限性一:HasQueryFilter方法過濾篩選無法應用於導航屬性。
接下來我們再來看一個例子,首先我們定義如下繼承關系:
public abstract class Payment { public int PaymentId { get; set; } public decimal Amount { get; set; } public string Name { get; set; } public bool IsDeleted { get; set; } } public class CashPayment : Payment { } public class CreditcardPayment : Payment { public string CreditcardNumber { get; set; } }
上述我們定義支付基類Payment,然后派生出現金支付CashPayment和信用卡支付CreditcardPayment子類,接下來我們配置如下查詢過濾:
builder.HasQueryFilter(f => !(f is CreditcardPayment && f.IsDeleted && ((CreditcardPayment)f).IsDeleted));
接下來我們進行如下查詢:
using (var context = new EFCoreDbContext()) { var payments = context.Payments.ToList(); }
沒毛病,接下來我們查詢子類CreditcardPayment,如下:
using (var context = new EFCoreDbContext()) { var payments = context.Payments.OfType<CashPayment>().ToList(); }
由上得知使用HasQueryFilter進行過濾篩選的局限性之二。
局限性二:HasQueryFilter方法過濾篩選只能定義在基類中,無法對子類進行過濾。
HasQueryFilter通用解決方案
不知我們是否思考過一個問題,若有很多實體都有其軟刪除場景,那么我們就都需要加上IsDeleted字段,同時還需要配置全局過濾器,如此重復性動作我們是否覺得厭煩,搬磚的我們的單從程序角度來看,搬磚的本源就是為了解放勞動生產率,提高工作效率,說的更通俗一點則是解決重復性勞動。此時為了解決上述所延伸出的問題,我們完全可抽象出一個軟刪除接口,如下:
public interface ISoftDleteBaseEntity { bool IsDeleted { get; set; } }
接下來讓所有需要進行軟刪除的實體繼承該接口,比如博客實體,如下:
public class Blog : ISoftDleteBaseEntity { public int Id { get; set; } public string Name { get; set; } public string Url { get; set; } public bool IsDeleted { get; set; } ...... }
為了解決重復性配置勞動,我們讓所有繼承自上述接口一次性在OnModelCreating方法中配置全局過濾,如此一來則免去了在每個對應實體映射類中進行配置,如下:
Expression<Func<ISoftDleteBaseEntity, bool>> expression = e => !e.IsDeleted; foreach (var entityType in modelBuilder.Model.GetEntityTypes() .Where(e => typeof(ISoftDleteBaseEntity).IsAssignableFrom(e.ClrType))) { modelBuilder.Entity(entityType.ClrType).HasQueryFilter(expression); }
願望是美好的,結果尼瑪拋出異常不支持該操作。看到上述lambda表達式都沒有翻譯過來,此時我們轉到定義看看。
public virtual EntityTypeBuilder HasQueryFilter([CanBeNullAttribute] LambdaExpression filter);
居然參數類型不是以表達式樹的形式給出,如果是如下形式則肯定可以。
public virtual EntityTypeBuilder HasQueryFilter([CanBeNullAttribute] Expression<Func<TEntity, TProperty>> filter);
所以問題出在無法翻譯lambda表達式,那么我們就來自動構建lambda表達式參數和主體,如下:
foreach (var entityType in modelBuilder.Model.GetEntityTypes() .Where(e => typeof(ISoftDleteBaseEntity).IsAssignableFrom(e.ClrType))) { modelBuilder.Entity(entityType.ClrType).Property<Boolean>("IsDeleted"); var parameter = Expression.Parameter(entityType.ClrType, "e"); var body = Expression.Equal( Expression.Call(typeof(EF), nameof(EF.Property), new[] { typeof(bool) }, parameter, Expression.Constant("IsDeleted")), Expression.Constant(false)); modelBuilder.Entity(entityType.ClrType).HasQueryFilter(Expression.Lambda(body, parameter)); }
至此美好願望得以實現,算是給出了一種通用解決方案。
總結
本節我們講述了EntityFramework Core 2.0中的新特性HasQueryFilter,使用此特性仍然存在局限性無法對導航屬性屬性進行過濾,對實體為繼承次模型,那么過濾篩選只能定義在基類中,但是最后我們給出了通用解決方案解決重復定義查詢過濾問題,希望給閱讀的您一點幫助。精簡的內容,簡單的講解,希望對閱讀的您有所幫助,我們明天再會。