淺談NETCore中的默認查詢過濾(如軟刪除)

我們知道,如果在業務界面上刪除一條數據,通常的做法是與後臺通信,從數據庫表中刪除掉這一條記錄,這種方式通常被稱爲硬刪除。然而這種方式會帶來一個弊端,即數據一旦刪除了,就真的永久刪除了,沒有後悔藥可以喫,也沒有辦法恢復。這樣,在一些場景中,比如需要保留用戶刪除的痕跡或能夠恢復刪除的數據的時候,硬刪除就沒有辦法滿足需求了。因此,相對於硬刪除,聰明的人們又想到了軟刪除。

軟刪除的概念

軟刪除又叫邏輯刪除或標記刪除,是一種對立於硬刪除的刪除方式。這種方式並不是真正的從數據庫表中把記錄刪除,而是通過特定的標記方式標記此記錄已被刪除(在查詢的時候過濾掉此記錄),這樣用戶在界面上看起來就像是數據真的被刪掉了,然而事實上卻是庫裏還在,甚至明天還想再見他。

設計軟刪除的原則與總結

在設計軟刪除的時候,一定要考慮業務上是否一定需要軟刪除,如果不需要請不要畫蛇添足。

當在業務上需要軟刪除的情況下,就要開始考慮業務的數據量和讀寫的比例,從表數據量的角度去分析、優化數據庫的性能。

雖然軟刪除從邏輯上看更加嚴謹,且能夠保證數據的完整性,但這並不代表我們在任何時候都要使用軟刪除。當我們確定某些數據真的不需要的時候,硬刪除是十分必要的,因爲這樣能減少數據庫表中的記錄數量,有效提升數據庫性能。
=============================================

上面講完了概念,下面我們開始講代碼怎麼來實現,我們以軟刪除爲例,其他舉一反三即可。

在代碼中實現軟刪除,我們要儘量做到底層數據庫上下文當中,避免業務層去寫,很容易忘記的,導致獲取的數據不正確。

要查詢默認軟刪除,我們在底層定義一個接口,這個接口在數據庫層、業務層都能使用到。

我們定義一個 ISoftDelete 的接口類

   public interface ISoftDelete
    {
         bool IsDeleted { get; set; }
    }

需要設置軟刪除的實體,都繼承該接口,如

   public class Project:EntityBase<int>,ISoftDelete
    {
      
        [DisplayName("用戶Id")]
        public string UserId { get; set; }

        [DisplayName("項目名稱")]
        public string TecName { get; set; }


        [DisplayName("是否刪除")]
        public bool IsDeleted { get; set; }

    }

EntityBase 是我定義了一個id 的基類。

基礎工作準備好後,默認查詢過濾主要在數據庫上下文中處理。

我們這邊介紹2種方式,一種是EF默認提供的,第二種是基於EF我們優化的。

一、EF提供的查詢過濾

EF提供了HasQueryFilter 方法來實現默認過濾。

我們可以在定義的數據庫上下文OnModelCreating方法中這麼寫

  protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<Project>().HasQueryFilter(m => m.IsDeleted);

        }

這樣就對每一個有設置的實體,進行了默認的查詢過濾。

那如果設置了之後,我們在一些場景下需要把已刪除的數據也查詢出來呢?那就需要用到EF提供忽略查詢過濾功能,如果設置忽略了,則該實體的查詢將全部不再默認過濾了。

如下代碼

_context.Set<Project>().IgnoreQueryFilters()
_context爲我們的數據庫上下文對象,通過設置 Set對象的 IgnoreQueryFilters 方法
該方法返回的是IQueryable 對象。後面要查詢直接 .Where() 即可。

但是這種方式有個弊端,就是在OnModelCreating ,我們每個實體都要去寫一遍,很繁瑣。

   下面我們就介紹第二種方式

二、利用反射自動添加DbSet 實體和過濾處理

這個原理也是比較簡單,就是對定義實體的庫的dll,通過反射,獲取裏面的實體類。然後遍歷,加入到數據庫上下文的DbSet中。同時,註冊過濾查詢。

重點在於代碼實現

下面這個方法,是從dll文件中反射,獲取實體類的集合。

        private List<Assembly> GetModelAssembly()
        {
            var list = new List<Assembly>();

            //var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type != "package");//排除所有的系統程序集、Nuget下載包
            //實體層的尋找有兩種方式,1種是配置,多個用英文逗號隔開,1種按照格式尋找,必須 *.Model的固定格式
            if (!string.IsNullOrWhiteSpace(ModelNames))  //ModelNames是實體類庫的名稱,比如Test.Model
            {
                string[] modelArr = ModelNames.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (var modelName in modelArr)
                {
                    Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(modelName));
                    list.Add(assembly);
                }
            }
            else
            {
                var deps = DependencyContext.Default;
                List<CompilationLibrary> libs = deps.CompileLibraries.Where(m => m.Name.Contains(".Model") && !m.Serviceable && m.Type != "package").ToList();
                if (libs.Any())
                {
                    foreach (var lib in libs)
                    {
                        if (lib != null)
                        {
                            Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name));
                            list.Add(assembly);

                        }
                    }
                }
            }

            return list;
        }

下面這個代碼是對實體類進行遍歷,寫入數據庫上下文的DbSet和過濾條件

        private ModelBuilder RegisteModel(ModelBuilder modelBuilder)
        {
            List<Assembly> assemblyList = GetModelAssembly();
            if (assemblyList.Any())
            {
                //對過濾特殊處理
                MethodInfo configureFilters = typeof(DbContextBase).GetMethod(nameof(ConfigureFilters), BindingFlags.Instance | BindingFlags.NonPublic); //DbContextBase爲自定義數據庫上下文對象
                if (configureFilters == null) throw new ArgumentNullException(nameof(configureFilters));

                foreach (var assembly in assemblyList)
                {
                    foreach (Type type in assembly.ExportedTypes)
                    {

                        if (type.IsClass && type != typeof(EntityBase<>) && !type.IsAbstract ) //默認找繼承EntityBase的,我們規定所有實體都繼承這個
                        {
                            if (typeof(ISoftDelete).IsAssignableFrom(type))  //如果實體繼承軟刪除接口,則調用註冊過濾的方法
                            {
                                //增加軟刪除的過濾
                                configureFilters.MakeGenericMethod(type).Invoke(this, new object[] { modelBuilder });
                            }
                            //  防止重複附加模型,否則會在生成指令中報錯
                            if (modelBuilder.Model.FindEntityType(type) != null)
                                continue;
                            // modelBuilder.Model.AddEntityType(type);
                            modelBuilder.Entity(type);  //將實體加入到DbSet當中
                            // modelBuilder.Model.AddEntityType(type);
                           


                        }
                    }
                }
            }
            return modelBuilder;
        }

 

     /// <summary>
        /// 自定義配置篩選方法
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="builder"></param>
        protected virtual void ConfigureFilters<TEntity>(ModelBuilder builder) where TEntity : class
        {
            Expression<Func<TEntity, bool>> expression = e => !EF.Property<bool>(e, "IsDeleted");
            builder.Entity<TEntity>().HasQueryFilter(expression);
}

通過以上代碼,就實現了自動註冊和過濾功能。

同樣的,若是要忽略過濾條件,只需要操作dbset對象即可。若在倉儲中沒有dbset,那需要把dbset定義一個public 對象出來使用。

如:

_projectRepository._dbSet.IgnoreQueryFilters()

代碼示例就是在底層Repository中定義了 DbSet類型的_dbSet變量,把這個變量公開出來,就可以操作了

若是有多個默認過濾的,比如租戶等,依次寫法優化即可。

以上就是簡單的,EF底層增加默認過濾的方式了。

 

推薦參考資料: https://www.sfjvip.com/csharp/8878.html

更多分享,請大家關注我的個人公衆號:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章