EF CORE 软删除
向应用添加软删除有如下步骤:
向需要软删除的实体类添加软删除属性
向 DbContext 中添加代码,以对这些实体类应用查询过滤器
如何设置/重置软删除
1.添加软删除属性
对于标准的软删除实现,你需要一个布尔标志来控制软删除,例如,这里有一个名叫 SoftDeleted 属性的 Book 实体。
public class Book : ISoftDelete
{
public int BookId { get; set; }
public string Title { get; set; }
//... 其它无关属性
public bool SoftDeleted { get; set; }
}
你可以通过它的名字SoftDeleted
来区分软删除属性,如果它的值是true
则该实体删除了,这意味着当你创建一个新实体时,它不会被软删除。
我还添加了一个 Book 类的ISoftDelete
接口(第 1 行),这个接口表示该类必须有一个可以读写的公共SoftDeleted
属性。这个接口将使得在DbContext
中配置软删除查询过滤器变得更加容易。
2.配置查询过滤器
你必须告诉 EF Core 哪个实体类需要一个查询过滤器,该过滤器是查询表达式,用来把不需要被看到的实体过滤掉。你可以在 DbContext 中使用以下代码手动完成此操作。
public class EfCoreContext : DbContext
{
public EfCoreContext(DbContextOptions<EfCoreContext> option)
: base(options)
{}
protected override OnModelCreating(ModelBuilder modelBuilder)
{
// 省略其它和软删除无关的代码
modelBuilder.Entity<Book>().HasQueryFilter(p => !p.SoftDeleted);
}
}
这很好,但是让我向你展示一种自动添加查询过滤器的方法。
在 DbContext 中的 OnModelCreating
方法中,你可以通过 Fluent API 配置 EF Core。但是也有一种方法可以判断每个实体类并决定如何配置它。在下面的代码中,你可以看到 foreach 循环依次遍历每个实体类,检查实体类是否实现了 ISoftDelete
接口,如果实现了,它将调用我创建的扩展方法来应用正确的软删除过滤器配置。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 省略其它无关的代码
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
// 省略其它无关的代码
if (typeof(ISoftDelete).IsAssignableFrom(entityType.ClrType))
{
entityType.AddSoftDeleteQueryFilter();
}
}
}
有许多配置可以直接应用于 GetEntityTypes 方法返回的类型,但是设置查询过滤器需要更多的工作。这是因为查询过滤器中的 LINQ 查询需要实体类的类型来创建正确的 LINQ 表达式。为此,我创建了一个小型扩展类,它可以动态创建正确的 LINQ 表达式来配置查询过滤器。
public static class SoftDeleteQueryExtension
{
public static void AddSoftDeleteQueryFilter(
this IMutableEntityType entityData)
{
var methodToCall = typeof(SoftDeleteQueryExtension)
.GetMethod(nameof(GetSoftDeleteFilter),
BindingFlags.NonPublic | BindingFlags.Static)
.MakeGenericMethod(entityData.ClrType);
var filter = methodToCall.Invoke(null, new object[] { });
entityData.SetQueryFilter((LambdaExpression)filter);
}
private static LambdaExpression GetSoftDeleteFilter<TEntity>()
where TEntity : class, ISoftDelete
{
Expression<Func<TEntity, bool>> filter = x => !x.SoftDeleted;
return filter;
}
}
我真的很喜欢这个操作,因为它可以节省我的时间,也避免我忘记配置每一个查询过滤器。
3.如何设置/重置软删除
将“软删除”属性设置为 true 很容易,对应的场景是用户选择一个条目并单击(软)“删除”,这会返回实体的主键。用代码实现如下:
var entity = context.Books.Single(x => x.BookId == id);
entity.SoftDeleted = true;
context.SaveChanges();
重置软删除属性在实际的业务场景中有点复杂。首先,你很可能想要向用户显示一个已删除实体的列表——把它想象成显示某个实体类类型的实例回收站,例如 Book。要做到这一点,需要在你的查询中使用IgnoreQueryFilters
方法,这意味着你将得到所有的实体(包括那些没有被软删除的和被软删除的),然后再根据需要选出那些 SoftDeleted
属性为 true 的。
var softDelEntities = _context.Books.IgnoreQueryFilters()
.Where(x => x.SoftDeleted).ToList();
相应的,当你收到一个重设 SoftDeleted
属性的请求时(它通常包含实体类的主键),则要加载此条目时,需要在查询中使用IgnoreQueryFilters
方法。
var entity = context.Books.IgnoreQueryFilters()
.Single(x => x.BookId == id);
entity.SoftDeleted = false;
context.SaveChanges();
使用软删除注意事项
首先,需要说的是查询过滤器是非常安全的。我的意思是,如果查询过滤器返回 false,那么特定的实体/行将不会包含在查询(包括 Find 和 Include 等)返回的结果集中。你可以使用直接 SQL 绕过它,但除此之外,EF Core 会隐藏你软删除的数据。
但有几点你需要注意。
小心软删除过滤器与其它过滤器的混合使用
查询过滤器非常适合于软删除,但是查询过滤器更适合于控制对数据组的访问。例如,假设您想要构建一个 Web 应用程序来为多个公司提供服务,比如工资单。在这种情况下,你需要确保 A 公司看不到 B 公司的数据,反之亦然。这种类型的系统称为多租户应用程序,而查询过滤器非常适合此类场景。
问题是,每个实体类型只允许使用一个查询过滤器,因此,如果您想在多租户系统中使用软删除,那么您必须将这两个部分结合起来形成查询过滤器——下面是查询过滤器的示例:
modelBuilder.Entity<MyEntity>()
.HasQueryFilter(x => !x.SoftDeleted
&& x.TenantId == currentTenantId);
这看上去很好,但是当你使用IgnoreQueryFilters
方法忽略软删除标记进行查询时,它会忽略整个查询过滤器,包括多租户部分。因此,如果不小心,还会显示多租户数据!
答案是为自己构建一个特定于应用程序的IgnoreSoftDeleteFilter
方法,如下所示:
public static IQueryable<TEntity> IgnoreSoftDeleteFilter<TEntity>(
this IQueryable<TEntity> baseQuery, string currentTenantId)
where TEntity : class, ITenantId
{
return baseQuery.IgnoreQueryFilters()
.Where(x => x.TenantId == currentTenantId)
}
这将忽略所有筛选器,然后把多租户筛选器添加回去。这将使它更容易更安全地处理显示/重置被软删除的实体。
更多的看这里吧:连接