65.9K
CodeProject 正在变化。 阅读更多。
Home

比较NHibernate和Entity Framework之间的透明延迟加载

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2012年1月14日

CPOL

7分钟阅读

viewsIcon

36951

downloadIcon

977

在本文中,我将解释什么是透明延迟加载,以及它在 NHibernate 和 Entity Framework 中的实现方式。

动机

在与 NHibernate 合作多年后,当我尝试 Entity Framework 时,我假设 Entity Framework 具有与 NHibernate 相同的透明延迟加载行为。但这个假设并不完全正确。Entity Framework 存在一些细微的行为差异。在本文中,我将解释什么是延迟加载,NHibernate 和 Entity Framework 如何实现它,以及它们的异同。

什么是延迟加载

引用自维基百科

延迟加载是一种在计算机编程中常用的设计模式,用于推迟对象初始化,直到需要使用该对象的时候。如果正确且恰当地使用,它可以提高程序的运行效率。延迟加载的相反是急切加载。

当应用程序中存在领域模型时,领域模型中的实体用于表示问题域中的业务实体。实体之间的关联等同于业务实体的关系或交互。领域模型的魅力在于它可以尽可能地模仿问题域。然而,内存中的领域对象是易变的。因此,我们需要数据库中的表来维持领域对象。领域对象需要被保存到数据库或从数据库加载回内存。从数据库加载领域对象的代码并非微不足道。有时,这类代码会占据开发人员在应用程序开发中的大部分时间。另一方面,加载领域对象的代码对于任何领域实体来说都非常相似。因此,引入 ORM 框架来提供统一的数据访问代码。ORM 框架将开发人员从繁琐的数据库访问代码中解脱出来,让他们能够专注于更重要的业务逻辑代码。通过使用 ORM 框架,领域对象可以通过开发人员友好的 API 进行加载或保存。

领域模型中的领域对象通常不是独立的。在大多数情况下,多个领域对象相互链接,形成一个大的领域对象图。当应用程序从数据库获取特定领域对象时,我们还需要加载相关领域实体的对象。延迟加载的出现是为了按需加载相关领域对象。通过这样做,开发人员可以直接使用领域对象,自然地使用它,而无需担心相关领域对象是如何加载的。支持延迟加载最简单的方法是在属性和方法中自定义代码,在需要使用相关领域对象时对其进行初始化。添加自定义延迟加载代码是一项繁重、耗时且容易出错的工作。幸运的是,这种横切代码可以通过 AOP 框架注入,例如 Windsor Castle AOP 框架。这被称为透明延迟加载。使用透明延迟加载的前提是使任何属性或方法都支持延迟加载。在 C# 中,这意味着将属性或方法标记为 virtual。延迟加载的好处是推迟查询执行到应用程序真正需要它的时刻,并减少应用程序的内存使用。

NHibernate 中的透明延迟加载工作原理

NHibernate 使用 Windsor Castle AOP 框架来实现延迟加载。让我们看看如何在 NHibernate 中实现延迟加载:

  1. 使类属性和方法可访问且可重载。
  2. 延迟加载只能添加到公共或受保护的虚拟属性或方法。因此,如果你的类属性和方法是私有的,你将无法使用延迟加载。另外,类字段不支持延迟加载。

    public class Employee
    {
        public virtual int EmployeeID { get; set; }
        public virtual string Name { get; set; }
        public virtual Iesi.Collections.Generic.ISet<Task> Tasks { get; set; }
    }
  3. 在 HBM 映射文件中启用延迟加载。
  4. lazy 属性默认情况下为 true,尽管你仍然可以在 HBM 映射文件中显式指定它。

    <?xml version="1.0" encoding="utf-8" ?>
    <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Demo" namespace="Demo">
      <class name="Employee">
        <id name="EmployeeID">
          <generator class="identity" />
        </id>
        <property name="Name" />
        <set name="Tasks" lazy="true" inverse="true" cascade="all-delete-orphan">
          <key column="EmployeeID"/>
          <one-to-many class="Demo.Task"/>
        </set>
      </class>
    </hibernate-mapping>
  5. 从 session 返回领域对象。
  6. 领域对象必须从 session 返回,例如使用带有领域实体 ID 的 Session.Get 方法,或者使用带有 HQL 查询的 Session.CreateQuery

    var employee = session.Get<Employee>(1);

    或者

    var employee = 
      session.CreateQuery("from Employee as e where e.EmployeeID = 1"
      ).List<Employee>().FirstOrDefault();

    如果领域对象已附加到 NHibernate session,则你无法对其进行延迟加载。内部,NHibernate 使用 Windsor Castle AOP 框架来实现延迟加载。Windsor Castle AOP 框架会继承领域类,并重写所有公共和受保护的虚拟属性和方法,以便在第一次访问时提供加载相关领域对象的逻辑。继承的类被命名为 <Class>Proxy

    Task Object in NHibernate

    Session.Get 方法是让 NHibernate 创建子类实例以替换真实领域对象的入口点。使用创建对象的代码对于代理对象和真实领域对象是相同的。延迟加载过程对开发人员来说是透明的;如果你想知道对象是否已加载,可以使用 NHibernateUtil.IsInitialized 来验证。

    Assert.IsFalse(NHibernateUtil.IsInitialized(task.Employee));

    如果你不想启用延迟加载(因为 N+1 性能问题),或者你知道你将在应用程序中使用所有相关对象,你可以通过将 lazy 属性设置为 false 来禁用延迟加载。

    <set name="Tasks" lazy="false" inverse="true" cascade="all-delete-orphan">
      <key column="EmployeeID"/>
      <one-to-many class="Demo.Task"/>
    </set>

    通过将 lazy 属性设置为 false,NHibernate 将为你隐式执行急切加载。

Entity Framework 中的透明延迟加载工作原理

Entity Framework 的早期版本没有透明延迟加载功能。这个功能是在 4.0 版本中引入的。由于没有提前设计透明延迟加载,Entity Framework 中的这个功能的设计方式有所不同,以保持与 Entity Framework 旧版本的向后兼容性。如果你的应用程序中有 POCO 领域类,你需要执行以下操作来在 Entity Framework 中启用延迟加载:

  1. 使类属性和方法可重写且可访问。
  2. 这一步与 NHibernate 类似,Entity Framework 使用子类将额外代码注入到领域类中以实现延迟加载。

    注意:这仅适用于 POCO 风格的领域类;如果你的领域类是使用 EntityModelCodeGenerator 模板生成的,它应该已经包含显式的延迟加载代码,因此不需要子类。下面是使用 EntityModelCodeGenerator 模板生成的一个代码片段。

    [XmlIgnoreAttribute()]
    [SoapIgnoreAttribute()]
    [DataMemberAttribute()]
    [EdmRelationshipNavigationPropertyAttribute("LazyLoadingModel", "FK_Task_Employee", "Task")]
    public EntityCollection<Task> Tasks
    {
        get
        {
            return ((IEntityWithRelationships)this).RelationshipManager.
              GetRelatedCollection<task>("LazyLoadingModel.FK_Task_Employee", "Task");
        }
        set
        {
            if ((value != null))
            {
                ((IEntityWithRelationships)this).RelationshipManager.
                   InitializeRelatedCollection<task>("LazyLoadingModel.FK_Task_Employee", "Task", value);
            }
        }
    }
  3. 在 *edmx* 属性中将“Lazy Loading Enabled”指定为 true。
  4. 你实际上可以在你的 context 类中直接指定 Lazy Loading Enabled。但是,默认位置是在 *edmx* 文件中。如果 Lazy Loading Enabled 设置是自动生成的,它最终会进入你的 context 类,就像我示例中的 LazyLoadingEntities 一样。

    public partial class LazyLoadingEntities : ObjectContext
    {
        public const string ConnectionString = "name=LazyLoadingEntities";
        public const string ContainerName = "LazyLoadingEntities";
    
        #region Constructors
    
        public LazyLoadingEntities()
            : base(ConnectionString, ContainerName)
        {
            this.ContextOptions.LazyLoadingEnabled = true;
        }
    
        public LazyLoadingEntities(string connectionString)
            : base(connectionString, ContainerName)
        {
            this.ContextOptions.LazyLoadingEnabled = true;
        }
    
        public LazyLoadingEntities(EntityConnection connection)
            : base(connection, ContainerName)
        {
            this.ContextOptions.LazyLoadingEnabled = true;
        }
    
        #endregion
    
        #region ObjectSet Properties
    
        public ObjectSet<Employee> Employees
        {
            get { return _employees  ?? 
              (_employees = CreateObjectSet<employee>("Employees")); }
        }
        private ObjectSet<employee> _employees;
    
        public ObjectSet<task> Tasks
        {
            get { return _tasks  ?? (_tasks = CreateObjectSet<task>("Tasks")); }
        }
        private ObjectSet<task> _tasks;
    
        #endregion
    }
  5. 从 context 返回领域对象。
  6. 需要延迟加载的领域对象必须从 context 返回。延迟加载的对象在 Visual Studio 2010 的 Watch 窗口中看起来是这样的。

    Task Object in Entity Framework

    如果你不想在 Entity Framework 中使用延迟加载,你可以通过将 LazyLoadingEnabled 设置为 false 来禁用它。

    this.ContextOptions.LazyLoadingEnabled = false;

    禁用延迟加载后,Entity Framework 将不再为你加载相关对象。如果你访问未加载的导航属性,你将收到一个 NullReferenceException 错误。或者,当你访问未加载的导航集合属性时,你会得到一个空集合。这在我用 NHibernate 几年后首次使用 Entity Framework 时让我感到困惑。

    Task Object in Entity Framework with Lazy Loading False

使用延迟加载的注意事项

在 NHibernate 和 Entity Framework 中,延迟加载默认是启用的。这在大多数情况下是最佳实践。通过启用延迟加载,开发人员无需担心相关对象是如何加载的。此外,未触及的引用对象或集合不会占用内存空间。但是,我们需要注意在某些情况下,延迟加载会引发 N+1 问题。N+1 问题是指当你为具有包含 N 个元素的集合属性的领域对象启用了延迟加载时,将执行一个 SELECT 查询来检索领域对象本身的数据,然后将为集合中的每个元素执行一个单独的查询。总共,你需要执行 N+1 个查询来获取领域对象和集合属性中的所有数据,而不是只有一个查询。这会导致过多的数据库查询。为了避免 N+1 问题,我们需要使用分析器来监控应用程序的执行情况,找出可能存在此问题的地方,然后使用显式急切加载或批处理加载来避免或缓解此问题。

Using the Code

代码是在 Visual Studio 2010 中开发的。你需要创建 SQL Server 中的 LazyLoading 数据库,然后运行附加的 CreateTables.sql 脚本来创建表和其中的数据。在运行代码之前,请记住在配置文件中更新连接字符串为你本地的连接字符串。

© . All rights reserved.