虚拟关键字,包括扩展方法,延迟加载,急切加载 - 加载相关对象如何实际工作

时间:2013-05-08 19:59:19

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

在MVC中加载相关对象可能会非常混乱。

在编写实体模型类和控制器时,如果您真的想知道自己在做什么,需要注意和学习很多术语。

我很长一段时间遇到的几个问题是:virtual关键字如何工作以及何时应该使用它? Include扩展方法如何工作以及何时应该使用它?

这就是我所说的;

virtual关键字:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace LazyLoading.Models
{
    public class Brand
    {
        public int BrandId { get; set; }
        public string Name { get; set; }
        public int ManufacturerId { get; set; }
        public virtual Manufacturer Manufacturer { get; set; }
    }
}

Include扩展方法:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using LazyLoading.Models;

namespace LazyLoading.Controllers
{
    public class LazyLoadingStoreController : Controller
    {
        private UsersContext db = new UsersContext();

        //
        // GET: /LazyLoadingStore/

        public ActionResult Index()
        {
            var brands = db.Brands.Include(b => b.Manufacturer);
            return View(brands.ToList());
        }

        ... Other action methods ...

请注意,Visual Studio会自动生成Index()操作方法。是的,Visual Studio自动添加了.Include(b => b.Manufacturer)。那太好了。

3 个答案:

答案 0 :(得分:11)

注意:写这个答案时我花了太长时间,只有当另外两个人出现时才丢弃它......

虚拟关键字与两个属性DbContext.Configuration一起使用:

  • ProxyCreationEnabled - 当EF
  • 创建对象时,允许EF crating动态代理
  • LazyLoadingEnabled - 允许动态代理在首次使用导航属性时加载相关实体

通过动态代理透明地实现延迟加载。动态代理是从您的实体派生的类,由EF在运行时创建和编译。它会覆盖您的虚拟导航属性,并在相关实体已加载或未加载的情况下实施逻辑检查。如果不是,则触发上下文加载(向数据库发出新查询)。延迟加载只能在实体所附加的上下文范围内执行 - 如果您处置上下文,则无法使用它。

动态代理创建由首先提到的属性控制。将实体实例创建为代理后,您无法“删除”代理。此外,如果您创建没有代理的实体(例如通过调用构造函数),您以后就无法添加它(但您可以使用DbSet<T>.Create而不是构造函数来获取代理实例。

第二个属性可以在实体实例的实时时间内更改,因此您可以通过将实体更改为false来避免对数据库进行不必要的查询(有时非常有用)。

Include表示急切加载。 Eager loading将相关实体与主实体一起加载,并作为主实体查询的一部分执行(它将SQL连接添加到查询中并构建一个大结果集)。

热切加载的好处是通过一次往返数据库来预先获取所有数据。特别是如果你知道你将需要所有这些,它可以是一种方法。如果您使用太多包含以及一些限制(您无法为加载的实体添加排序或过滤 - 它总是加载所有相关对象),则急切加载的缺点是very big result sets

延迟加载的好处是只在您真正需要时才加载数据。如果您真的不需要它们,那么如果您不了解它们会很有帮助。缺点是EF在某些情况下在您不期望它们时生成的其他查询(任何对该属性的首次访问都会触发延迟加载 - 即使导航集合上的Count也会触发加载所有数据以便能够执行在您的应用程序中计算,而不是从数据库中查询计数 - 这称为extra lazy loading,并且EF本身不支持它。另一个大问题是N + 1问题。如果您加载多个品牌,并且您将通过循环浏览所有已加载的品牌而不使用预先加载来访问其制造商产品,则您将向数据库生成N + 1个查询(其中N是品牌数量) - 一个用于加载所有品牌,一个用于加载每个品牌的制造商。

还有一个名为显式加载的选项。它就像延迟加载但它没有透明地执行。您必须使用上下文类自己执行它:

context.Entry(brand).Reference(b => b.Manufacturer).Load();

在这种情况下它不是很有用但是如果你在Manufacturer类上有Brands导航属性会很有用,因为你可以这样做:

var dataQuery = context.Entry(manufacturer).Collection(m => m.Brands).Query();

现在你有了一个IQueryable<Brand>实例,你可以添加任何条件,排序甚至是额外的急切加载,并对数据库执行它。

答案 1 :(得分:6)

我创建了一个测试MVC4互联网应用程序。

这是我发现的:

首先,创建实体模型类 - 注意virtual属性的Manufacturer关键字:

public class Manufacturer
{
    public int ManufacturerId { get; set; }
    public string Name { get; set; }
    public ICollection<Brand> Brands { get; set; }
}

public class Brand
{
    public int BrandId { get; set; }
    public string Name { get; set; }
    public int ManufacturerId { get; set; }
    public virtual Manufacturer Manufacturer { get; set; }
}

接下来,创建您的控制器 - 我使用CRUD操作方法和视图创建(使用创建新控制器对话框自动生成)。请注意由于Include模型类中的关系而由Visual Studio自动生成的Brand扩展方法。

public class LazyLoadingStoreController : Controller
{
    private UsersContext db = new UsersContext();

    //
    // GET: /LazyLoadingStore/

    public ActionResult Index()
    {
        var brands = db.Brands.Include(b => b.Manufacturer);
        return View(brands.ToList());
    }

现在让我们删除Include部分,以便我们的操作方法如下所示:

public ActionResult Index()
{
    var brands = db.Brands;
    return View(brands.ToList());
}

这是Index视图在添加几个Brand对象后在页面检查器中的显示方式 - 请注意Visual Studio会自动添加Manufacturer的下拉列表以及它如何自动搭建Name的{​​{1}}列 - 甜蜜!: enter image description here enter image description here

Manufacturer操作方法:

Create

真棒。一切都是为我们自动生成的!

现在,如果我们从// // GET: /LazyLoadingStore/Create public ActionResult Create() { ViewBag.ManufacturerId = new SelectList(db.Manufacturers, "ManufacturerId", "Name"); return View(); } 媒体资源中移除virtual关键字会怎样?

Manufacturer

这将会发生 - 我们的制造商数据已经消失:

enter image description here

好的,有道理。如果我添加public class Brand { public int BrandId { get; set; } public string Name { get; set; } public int ManufacturerId { get; set; } public Manufacturer Manufacturer { get; set; } } 扩展方法(Include仍然从virtual属性中移除),该怎么办?

Manufacturer

这是添加回public ActionResult Index() { var brands = db.Brands.Include(b => b.Manufacturer); return View(brands.ToList()); } 扩展方法的结果 - Include数据又回来了!:

enter image description here

这就是所有这些东西的运作方式。

接下来将解释在两种情况下(延迟加载和Eager加载)后台生成的T-SQL类型。我将留给别人。 :)

注意:在您添加Manufacturer关键字时,Visual Studio会自动生成Include(b => b.Manufacturer)

注意:哦,是的。差点忘了。以下是一些Microsoft资源的链接。

第二个链接讨论了另一个链接缺少的性能注意事项,如果这些事情可以帮助你。

答案 2 :(得分:1)

延迟加载

Brand是一个POCO(普通的旧CLR对象)。它是持久性无知。换句话说:它不知道它是由Entity Framework数据层创建的。它知道如何加载Manufacturer

但是,当你这样做时

var brand = db.Brands.Find(1);
var manufacturer = brand.Manufacturer;

Manufacturer即时加载(“懒惰”)。如果您监视发送到数据库的SQL,您将看到发出第二个查询以获取Manufacturer

这是因为在幕后,EF不会创建一个Brand实例,而是一个派生类型,一个代理,其中包含执行延迟加载的连线。这就是为什么启用延迟加载需要virtual修饰符的原因:代理必须能够覆盖它。

延迟加载通常用于智能客户端应用程序,其中上下文具有相对较长的生命周期(例如,每个表单的上下文)。虽然我必须说即使在使用短期上下文的智能客户端应用程序中也是有益且完全可能的。

急切加载

预先加载意味着您​​可以一次性加载具有粘附对象(父母和/或孩子)的对象。这就是Include方法的用途。在示例中

db.Brands.Include(b => b.Manufacturer)

您将看到EF使用联接创建SQL查询,并且访问Brand的{​​{1}}不再产生单独的查询。

在大多数情况下(特别是在断开连接的场景中),需要加载Eager,因为处理上下文实例的推荐方法是使用它们并为每个工作单元处理它们。因此,延迟加载不是一个选项,因为对于延迟加载的导航属性,上下文必须是活动的。