EFCore全局查询筛选器和软删除

2019-12-02 17:31:34  阅读 3453 次 评论 0 条

一、写在前面

   在使用EF进行数据查询时,我们使用Where进行条件筛选,但在项目开发中,我们经常使用软删除来进行数据的逻辑删除,但每次查询时都在Where条件中写判断数据是否被删除,是不是非常的头疼,尤其是我们前期没有进行判断,突然有一天发现了这个问题,那么对于我们来说简直是灾难性的,几乎每一个查询都需要进行修改,奈何EF没有提供全局查询筛选器。万幸的是微软爸爸看到我们开发人员如此的痛苦,勇敢的站了出来。在EF Core中加入了全局查询筛选器(HasQueryFilter),我们可以通过HasQueryFilter方法来创建过滤器,它能允许我们对特定数据库表的所有查询额外添加一模一样的过滤器,也就是说,它会在生成sql语句时额外为我们添加一些过滤信息。

二、使用全局查询筛选器

首先我们创建两个实体:Blog和Post,并用这两个实体演示如何使用HasQueryFilter进行全局查询筛选。

public class Blog
{
    public int Id { get; set; }
 
    public string Url { get; set; }
 
    public virtual ICollection<Post> Posts { get; set; }
}
 
public class Post
{
    public int Id { get; set; }
 
    public string Title { get; set; }
 
    public string Content { get; set; }
 
    public int BlogId { get; set; }
 
    public virtual Blog Blog { get; set; }
}

   我们在自定义的数据库上下文中的OnModelCreating()方法中来进行对Blog表创建全局查询筛选。

public class BloggingDbContext : DbContext
{
    public BloggingDbContext(DbContextOptions<BloggingDbContext> options) : base(options)
    {
 
    }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().HasQueryFilter(p => p.Id > 0);
 
        base.OnModelCreating(modelBuilder);
    }
 
    public DbSet<Blog> Blogs { get; set; }
 
    public DbSet<Post> Posts { get; set; }
}

 在全局筛选器中我们定义了BlogId > 0的所有数据,那么接下来我们来进行查询,并在SqlServer Profiler中看看最后生成的Sql语句。

var blogList = _dbContext.Blogs.ToList();
 
SELECT [p].[Id], [p].[Url]  FROM [Blogs] AS [p] WHERE [p].[Id] > 0

  可以看到,我们在查询Blog数据时,并没有使用Where语句来对数据进行筛选,但最后生成的Sql语句却带有筛选条件,这就说明我们定义的全局筛选已经生效了。那么接下来实现软删除是不是就很方便了。首先我们创建一个接口 IsDeleteSoft,并将所有的类都实现此接口。代码如下:

public interface IsSoftDelete
{
    bool IsDelete { get; set; }
}
 
public class Blog : IsSoftDelete
{
    public int Id { get; set; }
 
    public string Url { get; set; }
 
    public bool IsDelete { get; set; }
 
    public virtual ICollection<Post> Posts { get; set; }
   
}
 
public class Post : IsSoftDelete
{
    public int Id { get; set; }
 
    public string Title { get; set; }
 
    public string Content { get; set; }
 
    public int BlogId { get; set; }
 
    public virtual Blog Blog { get; set; }
 
    public bool IsDelete { get; set; }
}
 
public class BloggingDbContext : DbContext
{
    public BloggingDbContext(DbContextOptions<BloggingDbContext> options) : base(op
    {
 
    }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().HasQueryFilter(p => !p.IsDelete);
        modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDelete);
 
        base.OnModelCreating(modelBuilder);
    }
 
    public DbSet<Blog> Blogs { get; set; }
 
    public DbSet<Post> Posts { get; set; }
}

   我们这样写后,确实解决了全局筛选的问题,但问题又来了,如果我们有很多实体呢,我们总不能为每个实体都去实现ISoftDelete接口,并添加IsDelete字段,然后再为实体添加HasQueryFilter过滤器吧,这样来看,工作量并没有减少啊?这个时候我们的反射和表达式树就要开始登场了。下面来看看我们怎么实现的吧。

public interface IsSoftDelete
{
}
 
public class Blog : IsSoftDelete
{
    public int Id { get; set; }
 
    public string Url { get; set; }
 
    public virtual ICollection<Post> Posts { get; set; }
}
 
public class Post : IsSoftDelete
{
    public int Id { get; set; }
 
    public string Title { get; set; }
 
    public string Content { get; set; }
 
    public int BlogId { get; set; }
 
    public virtual Blog Blog { get; set; }
}

   我们定义了IsSoftDelete接口,所有实现此接口的实体都说明能够被软删除,但这次我们并没有定义IsDelete属性,那么我们怎样进行软删除呢?接下来我们为ModelBuilder实现一个扩展方法,在里面为所有实现IsSoftDelete接口的实体进行添加IsDelete属性以及HasQueryFilter查询过滤器。

public static class CustomModelBuilderExtensions
{
    public static ModelBuilder AddSoftDelete(this ModelBuilder modelBuilder)
    {
        //首先获取所有实现ISoftDelete接口的实体
        var entities = modelBuilder.Model.GetEntityTypes().Where(p => typeof(IsSoftDelete).IsAssignableFrom(p.ClrType));
 
        //循环遍历实体
        foreach (var entity in entities)
        {
            //如果实体存在IsDelete属性则获取,如果没有则先添加后获取
            entity.GetOrAddProperty("IsDelete", typeof(bool));
 
            //构建表达式树
            var parameter = Expression.Parameter(entity.ClrType);
            var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
            var isDeleteProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDelete"));
            BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeleteProperty, Expression.Constant(false));
            var lambdaExpression = Expression.Lambda(compareExpression, parameter);
 
            modelBuilder.Entity(entity.ClrType).HasQueryFilter(lambdaExpression);
        }
 
        return modelBuilder;
    }
}

然后我们只需要在数据库上下文的OnModelCreating()方法中调用此扩展方法就可以实现全局查询筛选了。

public static class CustomModelBuilderExtensions
{
    public static ModelBuilder AddSoftDelete(this ModelBuilder modelBuilder)
    {
        //首先获取所有实现ISoftDelete接口的实体
        var entities = modelBuilder.Model.GetEntityTypes().Where(p => typeof(IsSoftDelete).IsAssignableFrom(p.ClrType));
 
        //循环遍历实体
        foreach (var entity in entities)
        {
            //如果实体存在IsDelete属性则获取,如果没有则先添加后获取
            entity.GetOrAddProperty("IsDelete", typeof(bool));
 
            //构建表达式树
            var parameter = Expression.Parameter(entity.ClrType);
            var propertyMethodInfo = typeof(EF).GetMethod("Property").MakeGenericMethod(typeof(bool));
            var isDeleteProperty = Expression.Call(propertyMethodInfo, parameter, Expression.Constant("IsDelete"));
            BinaryExpression compareExpression = Expression.MakeBinary(ExpressionType.Equal, isDeleteProperty, Expression.Constant(false));
            var lambdaExpression = Expression.Lambda(compareExpression, parameter);
 
            modelBuilder.Entity(entity.ClrType).HasQueryFilter(lambdaExpression);
        }
 
        return modelBuilder;
    }
}

  到这里我们的全局查询筛选就算是完成了,但是这样做显然是不够完美的。为什么呢?因为我们仅仅进行了全局查询筛选,并且IsDelete字段并没有在实体中定义,那么我们的开发人员就没有办法像blog.Name这样来使用,那如果我们进行删除时,对开发人员来说是不太友好的。这显然不是我们想要的结果。那么我们应该怎么做呢?其实实现也是非常的简单,我们只需要在 dbContext.SaveChanges()和dbContext.SaveChangesAsync()来进行拦截并加工就可以。

public class BloggingDbContext : DbContext
{
    public BloggingDbContext(DbContextOptions<BloggingDbContext> options) : base(options)
    {
 
    }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.AddSoftDelete();
 
        base.OnModelCreating(modelBuilder);
    }
 
    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        OnBeforeSaving();
        return base.SaveChanges(acceptAllChangesOnSuccess);
    }
 
    public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = defau
    {
        OnBeforeSaving();
        return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }
 
    private void OnBeforeSaving()
    {
        foreach (var entry in ChangeTracker.Entries())
        {
            if (typeof(IsSoftDelete).IsAssignableFrom(entry.Entity.GetType()))
            {
                switch (entry.State)
                {
                    case EntityState.Deleted:
                        entry.State = EntityState.Modified;
                        entry.CurrentValues["IsDelete"] = true;
                        break;
                    case EntityState.Added:
                        entry.CurrentValues["IsDelete"] = false;
                        break;
                }
            }
        }
    }
 
    public DbSet<Blog> Blogs { get; set; }
 
    public DbSet<Post> Posts { get; set; }
}

  我们在SaveChanges()和SaveChangesAsync()之前进行拦截操作,如果实现了IsSoftDelete接口,那么在添加时,我们将会为IsDelete属性设置为False,在删除时,我们将实体状态设置为“修改”,并将IsDelete属性设置为true,那么我们提交到数据库的sql语句将会为update语句而不是delete。如果没有实现IsSoftDelete接口,那么我们不进行处理,提交到数据库的sql语句将会是delete。结合我们的全局查询筛选就可以实现软删除了。


微信扫码查看本文
本文地址:https://www.yangguangdream.com/?id=2061
版权声明:本文为原创文章,版权归 编辑君 所有,欢迎分享本文,转载请保留出处!

发表评论


表情

还没有留言,还不快点抢沙发?