浅谈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

更多分享,请大家关注我的个人公众号:

 

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