Moe*_*eri 26 c# soft-delete ef-code-first entity-framework-5
对于未来的访问者:对于EF6,您可能最好使用过滤器,例如通过此项目:https://github.com/jbogard/EntityFramework.Filters
在我们正在构建的应用程序中,我们应用"软删除"模式,其中每个类都有一个'已删除'布尔.实际上,每个类都只是继承自这个基类:
public abstract class Entity
{
public virtual int Id { get; set; }
public virtual bool Deleted { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
举一个简短的例子,假设我有类GymMember和Workout:
public class GymMember: Entity
{
public string Name { get; set; }
public virtual ICollection<Workout> Workouts { get; set; }
}
public class Workout: Entity
{
public virtual DateTime Date { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
当我从数据库中获取健身房成员列表时,我可以确保没有获取任何"已删除"健身房成员,如下所示:
var gymMembers = context.GymMembers.Where(g => !g.Deleted);
Run Code Online (Sandbox Code Playgroud)
然而,当我遍历这些健身房成员时,他们Workouts从数据库加载而不考虑他们的Deleted旗帜.虽然我不能责怪实体框架没有理解这一点,但我想以某种方式配置或拦截延迟属性加载,以便永远不会加载已删除的导航属性.
我一直在考虑我的选择,但它们看起来很稀缺:
这根本不是一种选择,因为手动工作太多了.(我们的应用程序非常庞大,每天都变得越来越大).我们也不想放弃使用Code First的优势(其中有很多)
再一次,不是一个选择.此配置仅适用于每个实体.总是急切地加载实体也会造成严重的性能损失.
我实际上在概念验证应用程序中对此进行了测试,并且它运行得非常好.这是一个非常有趣的选项,但是,它无法对延迟加载的导航属性应用过滤.这很明显,因为这些惰性属性不会出现在表达式/查询中,因此无法替换.我想知道实体框架是否允许在其DynamicProxy类中的某个位置加载延迟属性的注入点.我也担心其他后果,例如Include在EF 中打破机制的可能性.
Deleted自动过滤实体的自定义类.这实际上是我的第一种方法.我们的想法是为内部使用自定义Collection类的每个集合属性使用backing属性:
public class GymMember: Entity
{
public string Name { get; set; }
private ICollection<Workout> _workouts;
public virtual ICollection<Workout> Workouts
{
get { return _workouts ?? (_workouts = new CustomCollection()); }
set { _workouts = new CustomCollection(value); }
}
}
Run Code Online (Sandbox Code Playgroud)
虽然这种方法实际上并不差,但我仍然遇到一些问题:
它仍会将所有Workouts 加载到内存中,并在Deleted命中属性设置器时过滤掉它们.以我的拙见,这太迟了.
执行的查询与加载的数据之间存在逻辑不匹配.
想象一下我想要一个自上周以来做过锻炼的健身会员名单的情景:
var gymMembers = context.GymMembers.Where(g => g.Workouts.Any(w => w.Date >= DateTime.Now.AddDays(-7).Date));
Run Code Online (Sandbox Code Playgroud)
此查询可能会返回一个健身房成员,该成员只有已删除但仍满足谓词的锻炼.一旦将它们加载到内存中,就好像这个健身房成员根本没有锻炼!您可以说开发人员应该知道Deleted并始终将其包含在他的查询中,但这是我真正想要避免的.也许ExpressionVisitor可以再次提供答案.
Deleted,使用CustomCollection 标记导航属性是不可能的.想象一下这种情况:
var gymMember = context.GymMembers.First();
gymMember.Workouts.First().Deleted = true;
context.SaveChanges();`
Run Code Online (Sandbox Code Playgroud)
您可能希望Workout在数据库中更新相应的记录,那样你就错了!由于gymMember正在检查ChangeTracker任何变化,该财产gymMember.Workouts将突然返回少1锻炼.那是因为CustomCollection会自动过滤已删除的实例,还记得吗?所以现在Entity Framework认为需要删除锻炼,EF会尝试将FK设置为null,或者实际删除记录.(取决于数据库的配置方式).这是我们试图用软删除模式开始避免的!!!
我偶然发现了一个有趣的博客文章,它覆盖了默认SaveChanges方法,DbContext以便任何带有a的条目EntityState.Deleted都被改回,EntityState.Modified但这又感觉"hacky"而且相当不安全.但是,如果它解决了没有任何意外副作用的问题,我愿意尝试一下.
所以我在这里是StackOverflow.我已经非常广泛地研究了我的选择,如果我自己可以这样说的话,我就是在我的智慧结束.所以现在我转向你.您是如何在企业应用程序中实现软删除的?
重申一下,这些是我正在寻找的要求:
Deleted数据库级别的实体Deleted都应自动排除这些属性.我期待着任何和所有建议,谢谢你提前.
经过大量研究,我终于找到了实现我想要的方法.它的要点是我在对象上下文中使用事件处理程序拦截物化实体,然后在我可以找到的每个集合属性中注入我的自定义集合类(使用反射).
最重要的部分是拦截"DbCollectionEntry",这是负责加载相关集合属性的类.通过在实体和DbCollectionEntry之间摆动自己,我可以完全控制何时以及如何加载.唯一的缺点是这个DbCollectionEntry类几乎没有公共成员,这要求我使用反射来操纵它.
这是我的自定义集合类,它实现ICollection并包含对相应DbCollectionEntry的引用:
public class FilteredCollection <TEntity> : ICollection<TEntity> where TEntity : Entity
{
private readonly DbCollectionEntry _dbCollectionEntry;
private readonly Func<TEntity, Boolean> _compiledFilter;
private readonly Expression<Func<TEntity, Boolean>> _filter;
private ICollection<TEntity> _collection;
private int? _cachedCount;
public FilteredCollection(ICollection<TEntity> collection, DbCollectionEntry dbCollectionEntry)
{
_filter = entity => !entity.Deleted;
_dbCollectionEntry = dbCollectionEntry;
_compiledFilter = _filter.Compile();
_collection = collection != null ? collection.Where(_compiledFilter).ToList() : null;
}
private ICollection<TEntity> Entities
{
get
{
if (_dbCollectionEntry.IsLoaded == false && _collection == null)
{
IQueryable<TEntity> query = _dbCollectionEntry.Query().Cast<TEntity>().Where(_filter);
_dbCollectionEntry.CurrentValue = this;
_collection = query.ToList();
object internalCollectionEntry =
_dbCollectionEntry.GetType()
.GetField("_internalCollectionEntry", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(_dbCollectionEntry);
object relatedEnd =
internalCollectionEntry.GetType()
.BaseType.GetField("_relatedEnd", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(internalCollectionEntry);
relatedEnd.GetType()
.GetField("_isLoaded", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(relatedEnd, true);
}
return _collection;
}
}
#region ICollection<T> Members
void ICollection<TEntity>.Add(TEntity item)
{
if(_compiledFilter(item))
Entities.Add(item);
}
void ICollection<TEntity>.Clear()
{
Entities.Clear();
}
Boolean ICollection<TEntity>.Contains(TEntity item)
{
return Entities.Contains(item);
}
void ICollection<TEntity>.CopyTo(TEntity[] array, Int32 arrayIndex)
{
Entities.CopyTo(array, arrayIndex);
}
Int32 ICollection<TEntity>.Count
{
get
{
if (_dbCollectionEntry.IsLoaded)
return _collection.Count;
return _dbCollectionEntry.Query().Cast<TEntity>().Count(_filter);
}
}
Boolean ICollection<TEntity>.IsReadOnly
{
get
{
return Entities.IsReadOnly;
}
}
Boolean ICollection<TEntity>.Remove(TEntity item)
{
return Entities.Remove(item);
}
#endregion
#region IEnumerable<T> Members
IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
{
return Entities.GetEnumerator();
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return ( ( this as IEnumerable<TEntity> ).GetEnumerator() );
}
#endregion
}
Run Code Online (Sandbox Code Playgroud)
如果你浏览它,你会发现最重要的部分是"实体"属性,它会延迟加载实际值.在FilteredCollection的构造函数中,我为已经急切加载集合的场景传递了一个可选的ICollection.
当然,我们仍然需要配置实体框架,以便在有集合属性的任何地方使用FilteredCollection.这可以通过挂钩到Entity Framework的底层ObjectContext的ObjectMaterialized事件来实现:
(this as IObjectContextAdapter).ObjectContext.ObjectMaterialized +=
delegate(Object sender, ObjectMaterializedEventArgs e)
{
if (e.Entity is Entity)
{
var entityType = e.Entity.GetType();
IEnumerable<PropertyInfo> collectionProperties;
if (!CollectionPropertiesPerType.TryGetValue(entityType, out collectionProperties))
{
CollectionPropertiesPerType[entityType] = (collectionProperties = entityType.GetProperties()
.Where(p => p.PropertyType.IsGenericType && typeof(ICollection<>) == p.PropertyType.GetGenericTypeDefinition()));
}
foreach (var collectionProperty in collectionProperties)
{
var collectionType = typeof(FilteredCollection<>).MakeGenericType(collectionProperty.PropertyType.GetGenericArguments());
DbCollectionEntry dbCollectionEntry = Entry(e.Entity).Collection(collectionProperty.Name);
dbCollectionEntry.CurrentValue = Activator.CreateInstance(collectionType, new[] { dbCollectionEntry.CurrentValue, dbCollectionEntry });
}
}
};
Run Code Online (Sandbox Code Playgroud)
这看起来相当复杂,但它本质上是扫描物化类型的集合属性并将值更改为过滤集合.它还将DbCollectionEntry传递给过滤后的集合,以便它可以发挥其魔力.
这涵盖了整个"装载实体"部分.到目前为止唯一的缺点是,急切加载的集合属性仍将包含已删除的实体,但它们会在FilterCollection类的"Add"方法中被过滤掉.这是一个可以接受的缺点,虽然我还没有对它如何影响SaveChanges()方法进行一些测试.
当然,这仍然存在一个问题:查询没有自动过滤.如果您想要获取过去一周进行锻炼的健身会员,您希望自动排除已删除的锻炼.
这是通过ExpressionVisitor实现的,它自动将'.Where(e =>!e.Deleted)'过滤器应用于它在给定表达式中可以找到的每个IQueryable.
这是代码:
public class DeletedFilterInterceptor: ExpressionVisitor
{
public Expression<Func<Entity, bool>> Filter { get; set; }
public DeletedFilterInterceptor()
{
Filter = entity => !entity.Deleted;
}
protected override Expression VisitMember(MemberExpression ex)
{
return !ex.Type.IsGenericType ? base.VisitMember(ex) : CreateWhereExpression(Filter, ex) ?? base.VisitMember(ex);
}
private Expression CreateWhereExpression(Expression<Func<Entity, bool>> filter, Expression ex)
{
var type = ex.Type;//.GetGenericArguments().First();
var test = CreateExpression(filter, type);
if (test == null)
return null;
var listType = typeof(IQueryable<>).MakeGenericType(type);
return Expression.Convert(Expression.Call(typeof(Enumerable), "Where", new Type[] { type }, (Expression)ex, test), listType);
}
private LambdaExpression CreateExpression(Expression<Func<Entity, bool>> condition, Type type)
{
var lambda = (LambdaExpression) condition;
if (!typeof(Entity).IsAssignableFrom(type))
return null;
var newParams = new[] { Expression.Parameter(type, "entity") };
var paramMap = lambda.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement);
var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, lambda.Body);
lambda = Expression.Lambda(fixedBody, newParams);
return lambda;
}
}
public class ParameterRebinder : ExpressionVisitor
{
private readonly Dictionary<ParameterExpression, ParameterExpression> _map;
public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
_map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebinder(map).Visit(exp);
}
protected override Expression VisitParameter(ParameterExpression node)
{
ParameterExpression replacement;
if (_map.TryGetValue(node, out replacement))
node = replacement;
return base.VisitParameter(node);
}
}
Run Code Online (Sandbox Code Playgroud)
我的运行时间有点短暂,所以我稍后会回到这篇文章中了解更多细节,但是它的要点是写下来的,对于那些渴望尝试一切的人来说; 我在这里发布了完整的测试应用程序:https://github.com/amoerie/TestingGround
但是,可能仍然存在一些错误,因为这是一项非常重要的工作.虽然概念性的想法是合理的,但是一旦我整齐地重构了所有内容并且找到时间为此编写一些测试,我希望它能够很快完成.
| 归档时间: |
|
| 查看次数: |
5915 次 |
| 最近记录: |