在将所有导航属性加载(惰性或急切)到内存之前对其进行过滤

2024-05-07

对于未来的访问者:对于 EF6,您最好使用过滤器,例如通过此项目:https://github.com/jbogard/EntityFramework.Filters https://github.com/jbogard/EntityFramework.Filters

在我们正在构建的应用程序中,我们应用“软删除”模式,其中每个类都有一个“已删除”布尔值。实际上,每个类都简单地继承自该基类:

public abstract class Entity
{
    public virtual int Id { get; set; }

    public virtual bool Deleted { get; set; }
}

举一个简单的例子,假设我有课程GymMember and 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; }
}

当我从数据库中获取健身房会员列表时,我可以确保不会获取任何“已删除”的健身房会员,如下所示:

var gymMembers = context.GymMembers.Where(g => !g.Deleted);

然而,当我遍历这些健身房会员时,他们Workouts从数据库加载,不考虑它们的Deleted旗帜。虽然我不能责怪实体框架没有注意到这一点,但我想以某种方式配置或拦截延迟属性加载,以便永远不会加载已删除的导航属性。

我一直在考虑我的选择,但它们似乎很少:

  • 即将Database First并对每个对象的每个一对多属性使用条件映射 https://stackoverflow.com/questions/7044940/navigation-property-to-soft-deleted-entity.

这根本不是一个选择,因为这会需要太多的手动工作。 (我们的应用程序非常庞大,而且每天都在变得越来越大)。我们也不想放弃使用 Code First 的优势(其中有很多)

  • 总是急切地加载导航属性 http://msdn.microsoft.com/en-us/data/jj574232.aspx.

再说一次,这不是一个选择。此配置仅适用于每个实体。总是急切地加载实体也会造成严重的性能损失。

  • 应用自动注入的表达式访问者模式.Where(e => !e.Deleted)任何它发现IQueryable<Entity>,如上所述here https://stackoverflow.com/questions/17532393/use-expressionvisitor-to-exclude-soft-deleted-records-in-joins and here https://stackoverflow.com/questions/12760933/expressionvisitor-soft-delete.

我实际上在概念验证应用程序中对此进行了测试,效果非常好。 这是一个非常有趣的选项,但遗憾的是,它无法将过滤应用于延迟加载的导航属性。这是显而易见的,因为这些惰性属性不会出现在表达式/查询中,因此无法替换。我想知道实体框架是否允许在它们的某个地方有一个注入点DynamicProxy加载惰性属性的类。 我还担心其他后果,例如打破规则的可能性IncludeEF 中的机制。

  • 编写一个实现 ICollection 但过滤的自定义类Deleted自动实体。

这实际上是我的第一个方法。这个想法是为内部使用自定义 Collection 类的每个集合属性使用支持属性:

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); }
     }

}

虽然这种方法实际上还不错,但我仍然存在一些问题:

  • 它仍然加载所有Workouts 进入内存并过滤Deleted当属性设置器被击中时。以我的拙见,这已经太晚了。

  • 执行的查询和加载的数据之间存在逻辑不匹配。

想象一个场景,我想要一份自上周以来进行锻炼的健身房会员的列表:

var gymMembers = context.GymMembers.Where(g => g.Workouts.Any(w => w.Date >= DateTime.Now.AddDays(-7).Date));

此查询可能会返回仅具有已删除的锻炼但也满足谓词的健身房会员。一旦它们被加载到内存中,这位健身房会员就好像根本没有锻炼过一样! 您可以说开发人员应该意识到Deleted并始终将其包含在他的查询中,但这是我真正想避免的事情。也许 ExpressionVisitor 可以再次在这里提供答案。

  • 实际上不可能将导航属性标记为Deleted使用自定义集合时。

想象一下这个场景:

var gymMember = context.GymMembers.First();
gymMember.Workouts.First().Deleted = true;
context.SaveChanges();`

您会期望适当的Workout数据库中的记录已更新,您就错了!自从gymMember正在接受检查ChangeTracker如有任何变更,酒店gymMember.Workouts会突然减少 1 次锻炼。那是因为 CustomCollection 会自动过滤已删除的实例,还记得吗?所以现在实体框架认为需要删除该锻炼,并且 EF 将尝试将 FK 设置为 null,或者实际删除该记录。 (取决于您的数据库的配置方式)。这就是我们一开始就试图通过软删除模式来避免的!

我偶然发现了一个有趣的博客 http://blog.jorgef.net/2010/12/ef-soft-delete.html覆盖默认值的帖子SaveChanges的方法DbContext这样任何带有EntityState.Deleted被改回EntityState.Modified但这又让人感觉很“hacky”并且相当不安全。然而,如果它能解决问题并且没有任何意想不到的副作用,我愿意尝试一下。


我是 StackOverflow。如果我自己可以这么说的话,我已经对我的选择进行了相当广泛的研究,但我却束手无策。所以现在我转向你。您是如何在企业应用程序中实现软删除的?

重申一下,这些是我正在寻找的要求:

  • 查询应自动排除DeletedDB级别的实体
  • 删除实体并调用“SaveChanges”应该只是更新相应的记录,并且没有其他副作用。
  • 当加载导航属性时,无论是惰性的还是急切的,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
}

如果您浏览一下,您会发现最重要的部分是“Entities”属性,它将延迟加载实际值。在 FilteredCollection 的构造函数中,我为已经急切加载集合的场景传递了一个可选的 ICollection。

当然,我们仍然需要配置实体框架,以便我们的 FilteredCollection 可以在任何有集合属性的地方使用。这可以通过挂钩实体框架底层 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 });
            }
        }
    };

这一切看起来相当复杂,但它本质上所做的是扫描集合属性的物化类型并将值更改为过滤后的集合。它还将 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);
    }
}

我的时间有点紧迫,所以我稍后会回到这篇文章并提供更多详细信息,但它的要点已写下来,供那些渴望尝试一切的人使用;我在这里发布了完整的测试应用程序:https://github.com/amoerie/TestingGround https://github.com/amoerie/TestingGround

然而,可能仍然存在一些错误,因为这很大程度上是一项正在进行的工作。不过,这个概念想法很合理,我希望一旦我巧妙地重构了所有内容并找到时间为此编写一些测试,它就能很快完全发挥作用。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在将所有导航属性加载(惰性或急切)到内存之前对其进行过滤 的相关文章

随机推荐

  • 如何解决postgresql中group by和聚合函数的问题

    我正在尝试编写一个查询来划分两个 SQL 语句 但它显示了我 ERROR column temp missed must appear in the GROUP BY clause or be used in an aggregate fu
  • 如何将 OLE 自动化日期值转换为 SQL Server 中的日期

    我的应用程序存储日期作为 OLE 自动化与DateTime ToOADate 命令 我需要创建一个 SQL 视图来显示存储的日期 如何快速将双精度数转换为日期 Does SELECT CAST CASE WHEN OLEFLOAT gt 0
  • 为什么 MySQL 不允许我删除“更新 CURRENT_TIMESTAMP 时”属性?

    我有一个包含两个时间戳字段的表 我只是用名称和类型定义了它们TIMESTAMP 但由于某种原因 MySQL 自动设置其中一个默认值和属性on update CURRENT TIMESTAMP 我计划在这两个字段中都没有默认值 但其中一个字段
  • 如何在不更改源代码的情况下禁用 TLSv1?

    我编写了一个测试代码 不是 HTTPS 来使用 JDK8 测试 TLS 当测试代码运行时 我使用nmap工具扫描并得到结果如下 D softwares nmap 7 12 gt nmap p xxxx script ssl x x x x
  • 是否值得将 Doctrine 2 与 Zend Framework 一起使用?

    我知道有些用户在 Zend Framework 中使用 Doctrine 2 而不是 Zend Db 但我不知道为什么 为什么 Doctrine2 比 Zend Db 好 为什么 Zend Db 不好 Thanks 2013 年 3 月 7
  • Java中对象的序列化需要什么? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 谁能告诉我Java中对象序列化的需求是什么 并给我一个示例场景来解释需求 我已经了解什么是序列化 我只是想了解何时使用它以及如何使用它
  • 如何判断一个日期是否在其他两个日期之间?

    我有以下代码 if date in start end print in between else print No date start end都是变量 格式为1 1 我应该怎么做才能打印出正确的结果 我尝试将日期设置为 10 2 开始为
  • android中如何实现列表之间的拖放?

    我试图做到这一点 以便用户可以从一个列表视图中拖动文本视图 然后将其放入另一个列表视图中 但我发现这非常困难 到目前为止我发现的最大问题是 onTouchEvents 似乎只能在 ACTION DOWN 事件起源的视图中听到 我将单击一个列
  • 将项目添加到 JComboBox

    我在面板上使用组合框 据我所知 我们可以仅添加带有文本的项目 comboBox addItem item text 但有时我需要使用项目和项目文本的某些值 例如在 html select 中
  • Fortran 中的数组第一个索引

    我认为 Fortran 中数组的第一个索引是 1 但是为什么这段代码可以工作呢 代码是 Wavewatch 的修改部分 http polar ncep noaa gov waves wavewatch http polar ncep noa
  • 使用 Hibernate 映射数组

    你能帮我使用 Hibernate 映射这个类吗 public class MyClass private Long id private String name private int values 我使用的是 PostgreSQL 表中的
  • c/c++ strptime() 不解析 %Z 时区名称

    我是 C 语言的新手 当我练习 C 语言时 我会花时间来回构建 tm 我注意到一些不同 请告诉我我做错了什么 include
  • iOS:同时使用 Facebook 和 Google、Google Plus 登录

    早上好 我正在尝试在同一视图中实现 Facebook 登录 工作正常 以及 google plus 登录 我正在遵循官方网站 Google 的指南 但 Facebook 按钮和 google plus 按钮之间存在问题 Facebook 告
  • 开始使用 Python 进行安全 AWS CloudFront 流式传输

    我已经创建了一个 S3 存储桶 上传了一个视频 并在 CloudFront 中创建了一个流分配 用静态 HTML 播放器对其进行了测试 它可以工作 我已经通过帐户设置创建了密钥对 目前我的桌面上有私钥文件 这就是我所在的地方 我的目标是让我
  • 从远程存储库下载父pom

    是否可以 以及如何 从远程存储库下载父 pom 为什么 我有一个包含许多模块的项目设置 父 pom 指定对第三方 jar 的依赖关系 如果其中一个模块需要其中一个罐子的更高版本中的新功能 我想 参考新的 jar 更新父 pom 更新引用新父
  • 通过浏览器的“BACK”功能加载页面时触发 onload 脚本(js 或 jQuery)

    当通过浏览器的 后退 按钮或键盘命令到达页面时 我找不到执行脚本的方法 背景 我需要知道是否使用浏览器的 后退 按钮或键盘命令打开页面 然后我可以检查存储的 sessionStorage 变量并触发一些适当的内容 例如 如果我将其放入我的页
  • python:如何改变音量?

    I used winsound Beep frequency duration 因为我想以指定的频率播放声音 现在 我需要更改这些声音的音量 我怎样才能做到这一点 我试图寻求帮助pyaudio but 如果您对外部库开放 您可以使用pydu
  • Perl OO 方法调用第一个参数值 (->)

    就 Perl OO 而言 到底做了什么 gt do 例如我拨打 main 电话 result a b gt mymethod 在我定义的包中mymethod 我使用以下内容 my class 总的来说 我显然没有向mymethod 那么哪里
  • 来自 Step Function 的跨账户 Lambda 调用

    我在帐户 A 中有 Step Function 并且在帐户 B 中有 lambda 但是在运行 step 函数时 它给出 An error occurred while executing the state lambdaB entered
  • 在将所有导航属性加载(惰性或急切)到内存之前对其进行过滤

    对于未来的访问者 对于 EF6 您最好使用过滤器 例如通过此项目 https github com jbogard EntityFramework Filters https github com jbogard EntityFramewo