【问题标题】:Should Entity Framework lazy loading be disabled in web apps?是否应该在 Web 应用程序中禁用实体框架延迟加载?
【发布时间】:2019-01-27 07:33:59
【问题描述】:

我听说您应该在 Web 应用程序中禁用 EF 的延迟加载功能。 (ASP.NET)。 Herehere,对于初学者来说。

现在我真的很困惑,因为我一直认为应该始终启用延迟加载,因为它可以防止从数据库中获取不必要的数据。所以,现在我的问题是:一般在性能方面禁用 Web 应用程序中的延迟加载是否是一个更好的主意。如果是,您能解释一下原因吗?

【问题讨论】:

  • 我相信延迟加载会向 DB 引入一个查询,而不仅仅是一个。例如,您可以使用 Include() 在一个查询中加载所有数据。此外,如果您在 API 控制器中的输出位置使用相同的实体,序列化程序将始终遍历整个对象以完全序列化它,依次调用该实体上的延迟加载。
  • 这些例子根本没有说服力。他们展示了不应该延迟加载的代码,期间。无论是在 Web 应用程序中还是在有状态的客户端中,都应始终避免使用 n + 1 查询模式。也许您的问题不是基于意见的。如果您可以向我展示您绝对需要延迟加载的情况,那么是的,您需要延迟加载,无需讨论。在那之前,唯一的答案是:视情况而定。 IE。基于意见的,或广泛的,或不清楚的,无论如何,但不适合 Stack Overflow。

标签: c# asp.net asp.net-mvc entity-framework lazy-loading


【解决方案1】:

禁用延迟加载将防止 Select N+1 性能问题以及递归序列化 bail-outs,但是它用另一个工件空引用替换了这些问题。

在使用 Web 应用程序时,我不会禁用延迟加载,而是确保我的控制器/API返回实体,而是返回 ViewModel 或 DTO。当您采用 POCO 类来为您的视图提供数据量和它们所需的结构,并使用 .Select() 或 Automapper 的 ProjectTo<TViewModel>() 通过延迟执行来填充它们时,您无需担心延迟加载,并且为您的应用程序引入更好的整体性能和资源使用。从技术上讲,使用这种方法,可以禁用延迟加载,因此实际上并不是支持或反对禁用它,而是仅仅禁用延迟加载的行为不会让您的 Web 应用程序“更好”。

采用 ViewModel 具有许多优势:

  • 避免延迟加载调用或意外的 #null 引用。
  • 仅发送视图/消费者所需的数据,仅此而已。 (通过网络传输的数据更少,为具有调试视图的黑客提供的信息也更少。)
  • 为数据库服务器构建高效、可索引的查询。
  • 为不会触发 EF 的计算列提供一个位置。
  • 有助于减少返回途中的安全问题(意外的实体修改)。 (懒惰的开发人员不能简单地将视图模型重新附加并提交到 DB)

所以对我来说,Web 应用程序的问题不在于是否延迟加载,而只是为了避免将实体传递给客户端。我看到太多太多的“例子”在那里他们传递实体。我不认为这是一种健康的模式。

例如,使用视图模型时,第一个问题是“我的视图实际需要什么数据?”因此,给定一个产品和一个产品类别,如果我想发送一个产品实体,但我还需要产品类别名称,例如,如果我们的产品类别包含一系列产品,并且每个产品都有一个类别。当我们将我们的产品传递给序列化程序时,它会碰到那个循环引用,它要么会退出(留下引用或集合#null),要么会抛出异常。但是通过 Select N+1 我们将遍历 Product 属性,点击 ProductCategory 引用,然后“SELECT FROM ProductCategory WHERE ProductCategoryID = 3”。然后,当我们遍历该产品类别时,我们找到了另一个引用,那就是另一个 SELECT.... 等等。

通过使用视图模型,您可以将要检索的数据限制为视图需要的数据。我创建了一个产品视图模型,它概述了我关心的领域,无论数据来自哪里。如果我想要产品之类的东西,那就是名称和类别名称:

public class ProductViewModel
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string CategoryName { get; set; }
}

然后加载它:

var viewModel = context.Products
    .Where(x => x.ProductId == productId) 
    .Select(x => new ProductViewModel
    {
        ProductId = x.ProductId,
        ProductName = x.Name,
        CategoryName = x.Category.Name
    }).Single();

完成。不需要延迟加载或急切加载。这将生成一个查询,该查询返回一条记录,其中只有我们需要的 3 列。 (而不是整个产品记录,它是产品类别)

随着需求变得越来越复杂,我们可以引入视图模型层次结构,但我们可以继续根据视图的实际需要来扁平化相关数据。在某些情况下,这可能意味着选择匿名类型,然后将这些结果转换为视图模型,我们需要在其中使用 EF 无法转换为 SQL 的函数等。该方法是一种强大且快速的替代依赖加载实体的方法,但需要在开始时注意了解最终消费者(视图/API)需要从数据中获得什么。

【讨论】:

    【解决方案2】:

    在网络应用程序中,您有来自不同用户的大量并发请求,每个请求的处理速度都非常快(至少应该如此),因此您希望减少每个请求期间的数据库调用次数,因为每个数据库请求都会发生通过网络。使用延迟加载,每次使用关系属性时,都会再次调用 DB 将相关数据加载到实体集合中。因此,在一个 http 请求期间,您可以通过这种方式向 DB 发出 很多额外的请求,从性能的角度来看,这确实会损害您的应用程序。在正常情况下,当您最初从数据库中获取数据时,您已经知道需要哪些相关数据,因此您可以使用急切加载相关实体在一个请求中加载您需要的所有内容到 DB 来处理特定的 http 请求。

    【讨论】:

      【解决方案3】:

      我有一个遗留项目。当我在 sql profile 中检查有多少请求进入数据库时​​,我真的很惊讶。主页大约是 180(!)!主页只有两个列表,每个列表包含 20-30 项。 因此,您应该非常了解 N+1 个请求。您应该仔细检查代码审查它。对我来说,延迟加载会带来很多问题。当您使用该功能时,您永远不知道有多少请求进入数据库。

      【讨论】:

      • 你的论点是有道理的,但这里让我感到困惑的是,假设我们的系统中有一些产品类别,并且对于每个 HTTP 请求,我们必须从数据库中获取所有这些类别,如果我们禁用了延迟加载,当我们获取一个类别时,它的所有相关产品也会被获取,因为我们必须获取所有类别,我们实际上获取数据库中的所有产品!如果我们有一些与产品相关的实体,比如它的“品牌”,那就更糟了。所以基于这种方法,实际上,我们是为每个请求获取整个数据库,这样可以吗?
      • 如果您为每个请求获取整个数据库,这意味着您需要为每个请求提供整个数据库 - 这很奇怪。一般来说,当然你需要分离你的存储库方法,这会带来一些复杂性,例如,而不是一个获取方法 GetUsers (通过延迟加载,你可以进一步下载所有相关数据)你最终会得到像 GetUsersWithPurchasesGetUsersWithSettings 等等。因此,对于每个特定的用例,您都需要单独的获取逻辑。您不应该使用一种方法来为每个用例加载所有内容。
      • @AmirArbabian 不,我不需要每个请求的整个数据库,我说的是我的场景,我认为你误解了它,我说假设你有一个存在于数据库中的实体,并且对于每个请求,您需要从数据库中获取它,而不是延迟加载,当您从数据库中获取该实体时,您实际上是在再次获取其所有相关实体以及每个相关实体的相关实体,最终您结束整个数据库在内存中。我希望我说清楚了......
      • @AmirHosseinAhmadi 但如果你需要这个实体及其所有关系,这意味着你无论如何都会加载所有这些,但是通过延迟加载来实现你对数据库的请求很少而不是一个。
      • @AmirHosseinAhmadi,我使用 T-SQL。每个初级开发人员都可以编写 SQL 请求。或者我可以教它几个星期。它更简单。每个人都明白发生了什么。我有两种方法。如果 SQL 请求没有连接,我使用 ORM。或者如果需要插入一个大的复杂实体,我使用 ORM。这种情况下使用ORM更容易。但如果我需要一个或多个连接的复杂 SQL 请求,我更喜欢 SQL。
      【解决方案4】:

      延迟加载会产生 N+1 个问题。

      这是什么意思?

      这意味着对于每个加载的对象 + 初始查询,它将(至少)一次访问数据库。

      为什么不好?

      假设您有这些课程 MovieMovieGenre,并且您在 DB 中有 100 部电影,它们之间有 30 种类型

      public class Movie
          {
              public int Id { get; set; }
      
              public string Name { get; set; }
      
              public virtual MovieGenre MovieGenre { get; set; }
      
              public byte MovieGenreId { get; set; }
      }
      
      public class MovieGenre
          {
              public byte Id { get; set; }
      
              public string Name { get; set; }
          }
      

      现在假设您正在导航到将显示所有 100 部电影的页面(还记得 30 种电影类型吗?),数据库将执行 31 个查询(每种电影类型 30 个 + 电影 1 个) 并且查询将是这样的

      初始查询(+1 部分):

      SELECT Id, Name, MovieGenreId
      From Movie
      

      附加查询(N部分):

      -- 1
      SELECT Id, Name
      From MovieGenre
      Where Id = 1
      
      -- 2
      SELECT Id, Name
      From MovieGenre
      Where Id = 2
      
      -- 3
      SELECT Id, Name
      From MovieGenre
      Where Id = 3
      
      -- 4
      SELECT Id, Name
      From MovieGenre
      Where Id = 4
      .
      .
      .
      

      急切加载避免了所有这些混乱,并将使用一个具有正确连接的查询。

      由于您在此处使用 C#,因此您可能希望使用 a tool called glimpse 来进一步了解该问题。

      【讨论】:

      • 我明白你在说什么。但是我在这里遇到的问题是,例如,假设我想获取单一类型的数据,并且该类型有 10,000 部相关电影。通过预先加载,如果我获取该单一类型,那么它的 10,000 部电影的所有数据也会被获取。这不是问题吗?
      • 如果您获取所有类型的电影,除非您特别要求,否则不会获取。为什么?因为 Movie 类引用的是 MovieGenre 而不是相反
      • 在我的 MovieGenre 类的 EF 类中,我有一个 public List<Movie> Movies { get; set; }。不是每次我获取流派时都会获取吗?
      【解决方案5】:

      我认为您需要先问几个问题,然后再选择一种方式并拒绝另一种方式:

      1. 我的数据集有多大,我是否需要立即使用相关数据? (这又是根据您的需要而定)
      2. 正如其他人已经讲述过的 N+1 问题;但是,这会变得很重要,具体取决于数据集的大小。
      3. 往返服务器以获取相关数据是否实用?
      4. 然后需要数据。无论您想要实时版本还是缓存版本都可以?

      我对他人所有重要输入的贡献。

      【讨论】:

        【解决方案6】:

        我会说延迟加载肯定会帮助用户在数据很少使用时不获取数据。

        想象您在 Web 应用程序中拥有 Master => Details 场景,当您意识到用户对细节并没有那么感兴趣时。 (您可以分析和审核用户发起的请求)。无需预先加载每个主记录的详细信息。

        另一方面,如果细节是交互的主要部分,只需进行急切加载并获取整个 Master => 每个细节。

        除了延迟/急切加载,请确保以下几点:

        始终在服务器端启用分页并加载有限的数据/在用户逐页导航时根据用户请求加载数据。

        如果读取请求最多,请禁用 AutoTracking。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-10-11
          • 1970-01-01
          • 1970-01-01
          • 2011-09-10
          • 2015-01-11
          • 2011-02-06
          • 1970-01-01
          相关资源
          最近更新 更多