【问题标题】:How can data be streamed from a LINQ to Entities query?如何从 LINQ to Entities 查询流式传输数据?
【发布时间】:2019-07-25 14:12:58
【问题描述】:

我想知道如何使用 EF6 从 SQL Server 流式传输数据。

假设有这些类

  • 人员存储库
  • EFPerson(EF 型号)
  • DomainPerson(域模型)
  • PersonUsingClass

假设 PersonUsingClass 依赖于获取一堆 DomainPersons。 假设业务规则规定不允许 EFPerson 离开 PersonRepository。

通常我会有一个如下所示的存储库方法:

    public IEnumerable<DomainPerson> GetPeople()
    {
        using (var db = new efContext())
        {
            IQueryable efPeople = db.Person.Where(someCriteria);

            foreach (var person in efPeople)
            {
                yield return person.ToDomainPerson();
            }
        }
    }

使用我这里的代码,所有内容都会在执行 foreach 时加载到内存中。我可以通过将 IQueryable 返回给 PersonUsingClass 来实现流式传输,但这会将 EF 模型暴露给该类,这是不受欢迎的场景。

真的不可能隐藏 EF 模型,为什么同时流式传输数据?还是有什么我不知道的?

【问题讨论】:

  • 在该方法中没有记录被加载到内存中(除非 EF 在内部进行一些缓存)。如果您 foreach 覆盖该方法的结果,那么您将逐条迭代每条记录。如果您对该方法的结果调用 .ToList(), ToArray(),则记录将被加载到内存中。
  • 太棒了!一位团队成员让我确信我错了,因为我们可以通过这种方式传输数据。您可以在线找到 EF 的文档,其中指出“LINQ 查询总是在迭代查询变量时执行,而不是在创建查询变量时执行”。所以最后我们认为,一旦 foreach 被命中,所有东西都会被加载到内存中,然后这个 yield return 不会有任何有意义的效果,除了推迟映射“ToDomainPerson”。请创建您的评论作为答案,然后我可以接受。

标签: c# entity-framework-6


【解决方案1】:

您创建的方法迭代由 EF 创建的 IQueryable&lt;&gt; 对象。

IQueryable&lt;&gt; 变量已延迟执行,因此在内部,EF 只会在 IQueryable&lt;&gt; 被迭代时(即首次调用 .MoveNext() 时)调用数据库。

此外,如果您曾经使用 SqlDataReader 手动滚动数据库调用,您会发现可以逐个查询 .Read() 的结果,您不需要加载所有记录到内存中。 EF 可能以这种方式流式传输记录(这是我的假设,可能取决于您的特定 EF 设置)。

您的方法返回一个IEnumerable&lt;&gt; 对象,该对象也受到延迟执行的影响。通过调用 GetPeople() 创建 this 的实例不会导致数据库调用。

当您的方法的结果被迭代时,您将触发对内部IQueryable&lt;&gt; 对象的迭代并一一转换结果。

因此:

在该方法中没有记录被加载到内存中(除非 EF 在内部进行一些缓存)。如果您遍历该方法的结果,那么您将逐条遍历每条记录。

如果您对该方法的结果调用.ToList().ToArray(),则记录将被加载到内存中。

【讨论】:

  • 除非 EF 在内部进行一些缓存——嗯,它确实有,并且有两种方法可以防止这种情况发生(参见另一个答案)。
  • @GertArnold 你是绝对正确的。我对这个问题的解释导致我专注于迭代方面。我让 EF 缓存主题含糊不清,因为有很多变量在起作用,而且这个主题甚至可能需要一个额外的问题。例如,可以在上下文级别禁用跟踪,您不必使用.AsNoTracking()
  • 可以在上下文级别禁用跟踪,但只能在 ef-core 中。
【解决方案2】:

Entity Framework 查询过去是缓冲的,可以通过AsStreaming 扩展方法进行流式传输。但流式传输长期以来一直是默认设置,扩展方法仍然存在,但现在已过时(在 EF6 中)。就是这样一个。

但不要忘记 EF 的更改跟踪器。默认情况下,EF 缓存它在其更改跟踪器中实现的所有实体,这是一个身份缓存。因此,即使查询是流式的,为了防止内存消耗,您必须阻止 EF 跟踪实体。这正是您的代码中缺少的内容。

foreach 循环的每次迭代都会将一个 Person 实例附加到更改跟踪器。

可以通过两种方式防止缓存实体。

  1. 只需使用db.Person.AsNoTracking()
  2. 立即计划。投影创建 EF 不跟踪的类型的对象。

第二种方法如下所示:

var people = db.Person.Where(someCriteria).Select(p => p.ToDomainPerson());

当然ToDomainPerson() 不能翻译成SQL。相反,您应该执行以下操作:

db.Person.Where(someCriteria).Select(p => new DomainPerson
{
    Name = p.Name,
    ...
}
);

或者,更好的是,使用AutoMapper's ProjectTo 方法,它可以使您的代码与ToDomainPerson 方法一样干燥。

立即投影的好处是你只从数据库中拉取必填字段,之后不会触发延迟加载。延迟加载可能是序列化问题或异常的根源,因为在触发延迟加载时会释放上下文。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多