如何使用 Entity Framework 自动过滤掉软删除的实体?

Posted

技术标签:

【中文标题】如何使用 Entity Framework 自动过滤掉软删除的实体?【英文标题】:How can I automatically filter out soft deleted entities with Entity Framework? 【发布时间】:2012-09-23 19:24:12 【问题描述】:

我首先使用实体​​框架代码。我在DbContext 中覆盖SaveChanges 以允许我执行“软删除”:

if (item.State == EntityState.Deleted && typeof(ISoftDelete).IsAssignableFrom(type))

    item.State = EntityState.Modified;
    item.Entity.GetType().GetMethod("Delete")
        .Invoke(item.Entity, null);

    continue;

这很好,所以对象知道如何将自己标记为软删除(在这种情况下,它只是将IsDeleted 设置为true)。

我的问题是我怎样才能使它在检索对象时忽略IsDeleted 中的任何对象?所以如果我说_db.Users.FirstOrDefault(UserId == id) 如果那个用户有IsDeleted == true 它会忽略它。本质上我想过滤?

注意:我不想只是把&& IsDeleted == true 这就是为什么我用接口标记类,以便删除知道如何“正常工作”,我想以某种方式修改检索以了解如何也基于存在的接口“正常工作”。

【问题讨论】:

除非我误解了你,否则你只需在你的 Linq 查询中添加另一个子句。也就是说,你做到了FirstOrDefault(UserId == id && !IsDeleted) 或者你使用一个已经被过滤的IQueryable,比如_repository.ActiveUsers.FirstOrDefault(UserId == id) @Arran 是的,我希望能够避免这样做,这样我就不必知道在我的代码中哪些类被软删除了。我让软删除类使用接口 ISoftDelete,因此当删除它完成然后保存更改时,它会看到它实现了该接口并处理软删除。有没有类似的方法来处理检索? 你可以尝试实现类似的东西。或者您可以简单地在 Visual Studio 中进行查找和替换。 :) @RobertHarvey 但是如果我使用IQueryable 我会失去像 Add() 这样的东西我想要DbSet 的所有好处但能够过滤它们:) 我不想拥有DbSet 代表我的用户,IQueryable 代表活跃用户或类似的东西(如果这是你的建议) 【参考方案1】:

我的所有实体都可以进行软删除,并且使用this answer 建议的技术不会通过上下文检索软删除项目。这包括当您通过导航属性访问实体时。

为每个可以软删除的实体添加一个 IsDeleted 鉴别器。不幸的是,我还没有根据派生自抽象类或接口 (EF mapping doesn't currently support interfaces as an entity) 的实体来解决这个问题:

protected override void OnModelCreating(DbModelBuilder modelBuilder)

   modelBuilder.Entity<Foo>().Map(m => m.Requires("IsDeleted").HasValue(false));
   modelBuilder.Entity<Bar>().Map(m => m.Requires("IsDeleted").HasValue(false));

   //It's more complicated if you have derived entities. 
   //Here 'Block' derives from 'Property'
   modelBuilder.Entity<Property>()
            .Map<Property>(m =>
            
                m.Requires("Discriminator").HasValue("Property");
                m.Requires("IsDeleted").HasValue(false);
            )
            .Map<Block>(m =>
            
                m.Requires("Discriminator").HasValue("Block");
                m.Requires("IsDeleted").HasValue(false);
            );

覆盖 SaveChanges 并找到所有要删除的条目:

编辑 Another way to override the delete sql是改EF6生成的存储过程

public override int SaveChanges()

   foreach (var entry in ChangeTracker.Entries()
             .Where(p => p.State == EntityState.Deleted 
             && p.Entity is ModelBase))//I do have a base class for entities with a single 
                                       //"ID" property - all my entities derive from this, 
                                       //but you could use ISoftDelete here
    SoftDelete(entry);

    return base.SaveChanges();

SoftDelete 方法直接在数据库上运行 sql,因为鉴别器列不能包含在实体中:

private void SoftDelete(DbEntityEntry entry)

    var e = entry.Entity as ModelBase;
    string tableName = GetTableName(e.GetType());
    Database.ExecuteSqlCommand(
             String.Format("UPDATE 0 SET IsDeleted = 1 WHERE ID = @id", tableName)
             , new SqlParameter("id", e.ID));

    //Marking it Unchanged prevents the hard delete
    //entry.State = EntityState.Unchanged;
    //So does setting it to Detached:
    //And that is what EF does when it deletes an item
    //http://msdn.microsoft.com/en-us/data/jj592676.aspx
    entry.State = EntityState.Detached;

GetTableName 返回要为实体更新的表。它处理表链接到 BaseType 而不是派生类型的情况。我怀疑我应该检查整个继承层次结构...... 但是有计划improve the Metadata API,如果我有必要会调查EF Code First Mapping Between Types & Tables

private readonly static Dictionary<Type, EntitySetBase> _mappingCache 
       = new Dictionary<Type, EntitySetBase>();

private ObjectContext _ObjectContext

    get  return (this as IObjectContextAdapter).ObjectContext; 


private EntitySetBase GetEntitySet(Type type)

    type = GetObjectType(type);

    if (_mappingCache.ContainsKey(type))
        return _mappingCache[type];

    string baseTypeName = type.BaseType.Name;
    string typeName = type.Name;

    ObjectContext octx = _ObjectContext;
    var es = octx.MetadataWorkspace
                    .GetItemCollection(DataSpace.SSpace)
                    .GetItems<EntityContainer>()
                    .SelectMany(c => c.BaseEntitySets
                                    .Where(e => e.Name == typeName 
                                    || e.Name == baseTypeName))
                    .FirstOrDefault();

    if (es == null)
        throw new ArgumentException("Entity type not found in GetEntitySet", typeName);

    _mappingCache.Add(type, es);

    return es;


internal String GetTableName(Type type)

    EntitySetBase es = GetEntitySet(type);

    //if you are using EF6
    return String.Format("[0].[1]", es.Schema, es.Table);

    //if you have a version prior to EF6
    //return string.Format( "[0].[1]", 
    //        es.MetadataProperties["Schema"].Value, 
    //        es.MetadataProperties["Table"].Value );

我之前在迁移中使用如下代码在自然键上创建了索引:

public override void Up()

    CreateIndex("dbo.Organisations", "Name", unique: true, name: "IX_NaturalKey");

但这意味着您无法创建与已删除组织同名的新组织。为了允许这样做,我更改了创建索引的代码:

public override void Up()

    Sql(String.Format("CREATE UNIQUE INDEX 0 ON dbo.Organisations(Name) WHERE IsDeleted = 0", "IX_NaturalKey"));

从索引中排除已删除的项目

注意 如果相关项目被软删除,则不会填充导航属性,但外键是。 例如:

if(foo.BarID != null)  //trying to avoid a database call
   string name = foo.Bar.Name; //will fail because BarID is not null but Bar is

//but this works
if(foo.Bar != null) //a database call because there is a foreign key
   string name = foo.Bar.Name;

P.S.在这里投票支持全局过滤https://entityframework.codeplex.com/workitem/945?FocusElement=CommentTextBox#,过滤后的包括here

【讨论】:

从6.1开始,查找表名容易一点:romiller.com/2014/04/08/ef6-1-mapping-between-types-tables 对使用它有什么想法,但也有能力在极少数情况下在需要时发出返回软删除记录的查询?例如,我希望我的应用永远不会包含逻辑删除的查找值,除非在我管理它们的管理页面上。 当 base.SaveChanges 失败并且所有更改都被还原时遇到问题,除了软删除的实体,因为我们使用 ExecuteSqlCommand 删除它们。 ExecuteSqlCommand 在与 SaveChanges 不同的事务中运行。【参考方案2】:

使用EntityFramework.DynamicFilters。它允许您创建将在执行查询时自动应用的全局过滤器(包括针对导航属性)。

项目页面上有一个示例“IsDeleted”过滤器,如下所示:

modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);

该过滤器将在针对 ISoftDelete 实体的任何查询中自动注入 where 子句。过滤器在您的 DbContext.OnModelCreating() 中定义。

免责声明:我是作者。

【讨论】:

此解决方案提供了一个非常简单、易于使用的界面,特别是如果您希望对上下文代码文件的影响最小。打开和关闭这些过滤器的能力在各种用例中特别有用。干得好! 您能否确认 EntityFramework.DynamicFilters 在遍历集合导航属性时有效? 这个解决方案很好,但是在我的项目中我想实现 feture to Recovery Soft Deleted ,我该怎么做?这样我就有了 SoftDelete=0 的记录,? 您可以在需要时暂时禁用过滤器。这在 github 项目页面上有描述。 EF Core 有类似的东西吗?【参考方案3】:

一种选择是将!IsDeleted 封装到扩展方法中。像下面这样的只是一个例子。注意它只是给你一个扩展方法的想法,下面不会编译。

public static class EnumerableExtensions

    public static T FirstOrDefaultExcludingDeletes<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    
        return source.Where(args => args != IsDeleted).FirstOrDefault(predicate);
    

用法:

_db.Users.FirstOrDefaultExcludingDeletes(UserId == id)

【讨论】:

这是一个很好的答案,所以 +1,它可以解决我的一些问题,但不能解决所有问题。我想它需要更深入地了解这种关系。因此,如果我拉一个为该组加载用户的组,它不会加载已删除的用户。 我猜你可以创建另一个扩展方法 ExcludeDeletes 并执行类似 _db.Groups.ExcludeSoftDeletes(groupId == id).Users.ExcludeSoftDeletes(UserId == id) 谢谢,但我真的不想为此编写一个新的 API,再次覆盖一些控制检索哪些数据的内部方法,也许将内部检索挂钩到视图或其他东西是理想的,因为这不会迫使我重写 EF API。假设我想要 Single 或 Any,我需要为所有内容编写扩展。不过好主意!谢谢 公平点;所有 EF API 都以任何方式将 (Func 谓词) 作为参数,因此您可以将 !IsDeleted 作为谓词传递【参考方案4】:

您可以在 Entity Framework Core 2.0 上使用 Global Query Filters。

protected override void OnModelCreating(ModelBuilder modelBuilder)

    modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");

    // Configure entity filters
    modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
    modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);

【讨论】:

【参考方案5】:

好问题。

您需要在 SQL 查询以某种方式执行之前拦截它,然后添加额外的 where 子句以从选择中删除“已删除”项目。不幸的是,Entity 没有可用于更改查询的 GetCommand。

也许可以修改位于正确位置的 EF Provider Wrapper 以允许查询更改。

或者,您可以使用 QueryInterceptor 但每个查询都必须使用 InterceptWith(visitor) 来更改表达式...

因此,我将专注于这种方法,因为 AFAIK 没有其他选择,然后拦截查询并修复它(如果您想保持查询的代码不变)。

无论如何,如果您发现有用的信息,请告诉我们。

【讨论】:

以上是关于如何使用 Entity Framework 自动过滤掉软删除的实体?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Entity Framework 自动过滤掉软删除的实体?

Entity Framework 5.0 Code First全面学习

使用 Entity Framework Core 时,通过代码自动进行 Migration

Entity Framework - CTP4 - Code First - 如何关闭自动复数?

如何告诉 Entity Framework 我的 ID 列是自动递增的(AspNet Core 2.0 + PostgreSQL)?

Entity Framework 5.0系列之自动生成Code First代码