我正在使用Entity Framework Code First。我在SaveChanges
覆盖了DbContext
,允许我进行“软删除”:
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
这就是为什么我用界面标记类,所以删除知道如何“正常工作”,我想以某种方式修改检索,知道如何“Just Work”也基于存在的界面。
答案 0 :(得分:36)
我的所有实体都使用了软删除功能,并且使用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
答案 1 :(得分:35)
使用EntityFramework.DynamicFilters。它允许您创建全局过滤器,这些过滤器将在执行查询时自动应用(包括针对导航属性)。
有一个例子&#34; IsDeleted&#34;在项目页面上过滤如下所示:
modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);
该过滤器将针对ISoftDelete实体的任何查询自动注入where子句。过滤器在DbContext.OnModelCreating()。
中定义免责声明:我是作者。
答案 2 :(得分:7)
一种选择是将!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)
答案 3 :(得分:1)
您可以在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);
}
答案 4 :(得分:-3)
很棒的问题。
您需要在以某种方式执行之前拦截SQL查询,然后添加其他where子句以从选择中删除“已删除”项。不幸的是,Entity没有可用于更改查询的GetCommand。
也许可以修改位于正确位置的EF Provider Wrapper以允许更改查询。
或者,你可以利用QueryInterceptor,但每个查询都必须使用InterceptWith(visitor)
来改变表达式......
所以,我会专注于这种方法,因为AFAIK没有其他选项,然后拦截查询并修复它(如果你想保持查询不变的代码)。
无论如何,如果你想出一些有用的东西,请告诉我们。