NHibernate N+1问题实例分析和优化

1.问题的缘起

考察下面的类结构定义

public class Category
    {
        string _id;
        Category _parent;
        IList<Category> _children = new List<Category>();

        public virtual string Id
        {
            get
            {
                return _id;
            }
        }

        public virtual Category Parent
        {
            get
            {
                return _parent;
            }
        }

        public virtual IList<Category> Children
        {
            get
            {
                return _children;
            }
        }

        public virtual string Title
        {
            get;
            set;
        }

        public virtual string ImageUrl
        {
            get;
            set;
        }

        public virtual int DisplayOrder
        {
            get;
            set;
        }
    }

  

其Nhibernate映射文件的内容为:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2">
  <class name="Category">
    <id name="Id" access="nosetter.camelcase-underscore" length="32">
      <generator class="uuid.string"/>
    </id>
    <property name="Title" not-null="true" length="50"/>
    <property name="ImageUrl" length="128"/>
    <property name="DisplayOrder" not-null="true"/>
    <many-to-one name="Parent" class="Category" column="ParentId" access="nosetter.camelcase-underscore"/>
    <bag name="Children" access="nosetter.camelcase-underscore" cascade ="all-delete-orphan" inverse="true" order-by="DisplayOrder ASC">
      <key column="ParentId"/>
      <one-to-many class="Category"/>
    </bag>
  </class>
</hibernate-mapping>

程序中要求“筛选出所有Category,再依次遍历其下的Children中的子对象”时,通常,我们会写出如下符合要求的代码

var query = from o in CurrentSession.QueryOver<Category>() select o;
IList<Category> list = query.List(); //第一级查询
foreach (Category item in list)
{
  foreach (Category child in item.Children) //第二级查询
  {
    //...
  }
}

这段代码运行正常,输出的SQL语句类似于:

--第一级查询
Select * From [Category]
--第二级查询(每次参数都不同)
Select * From [Category] Where [ParentId][email protected]
Select * From [Category] Where [ParentId][email protected]
.......
Select * From [Category] Where [ParentId][email protected]

从输出的SQL可以看出,上面的代码隐藏着严重的性能问题。假设第一级查询返回20个Category对象保存到list列表中,而每个Category中又包含10个子对象,那么两个foreach循环执行下来,共需要向数据库发送20*10=200条Select查询语句。对于list列表中的每个Category来说,从数据库中取出其本身需要执行一条Select语句(即第一级查询),查询其下的子元素需要执行10条Select语句,也就是说从取出Category到遍历完其所有子对象,需要执行N+1条Select语句,N是子对象的个数,这就是所谓的“N+1”问题,它最大的弊端显而易见,在于向数据库发送了过多的查询语句,造成不必要的开销,而通常情况下这是可以优化的。

2.解决方案

2.1 批量加载

由于“N+1”问题是发送了过多的Select语句,首先就会想到,能不能把这些语句合并在一次数据库查询中,为了解决这个问题,Nhibernate在集合映射中,提供了“批量加载”策略,即:batch-size,经改造后的bag映射如下:

<bag name="Children" access="nosetter.camelcase-underscore" cascade ="all-delete-orphan" inverse="true" order-by="DisplayOrder ASC" batch-size="20">
  <key column="ParentId"/>
  <one-to-many class="Category"/>
</bag>

batch-size表明Category.Children列表装载时,每次读取20个子对象,而不是一个一个加载。因此,N+1就演变成N/20+1。对于上文的两个foreach过程,输出的SQL语句类似于:

--第一级查询
Select * From [Category]
--第二级查询
Select * From [Category] Where [ParentId] In (@p0,@[email protected])

对比未采用“批量加载”策略的SQL输出,显然新的解决方案能够极大的减少向数据库发送查询语句。
如果需要将多有对象加载过程都设置为批量,可以在Nhibernate配置文件中添加default_batch_fetch_size属性,而不需要修改每个类的映射文件。
 

2.2 预加载

解决“N+1”问题的另一种方法是使用预加载(Eager Fetching),同样,Nhibernate在集合映射中也提供了对它的支持,即:outer-join或fetch。改造后的bag映射配置如下:

<bag name="Children" access="nosetter.camelcase-underscore" cascade ="all-delete-orphan" inverse="true" order-by="DisplayOrder ASC" outer-join="true">
 <key column="ParentId"/>
 <one-to-many class="Category"/>
</bag>

outer-join=“true”等效于fetch="join",而fetch还有“select”和“subselect”两个选项(默认为“select”选项),他们指的都是用何种SQL语句加载子集合。当outer-join=“true”或fetch="join"时,输出的SQL语句类似于:

--第一级查询
Select t0.Id,t0.ParentId,t0.Title...t1.Id,t1.ParentId,t1.Title... From [Category] t0 Left Join [Category] t1 On t1.ParentId=t0.Id

预加载在第一级查询,就通过Join一次性的取出对象本身及其子对象,比使用批量加载生成的语句还要少,首一次加载效率高。

虽然,在映射文件中启用预加载设置,十分简单,但是考虑到其他方式(如:Get或Load)获取对象时也会自动装载子对象,造成不必要的性能损失,另外,在映射文件中设置预加载,其“作用域”有只适用于:通过Get或Load获取对象、延迟加载(隐式)关联对象、Criteria查询和带有left join fetch的HQL语句,因此通常要求避免将启用预加载的配置写在映射文件里(Nhibernate也不推荐写在映射文件中),而是将其写在需要用到预加载的代码中,其他的地方则保持原有逻辑,这样才不会产生不良影响,预加载在代码里的写法有三种:

ICriteria.SetFetchMode(string associationPath, FetchMode mode);

或者在3.0里面使用的

IQueryOver<TRoot, TSubType>.Fetch(Expression<Func<TRoot, object>> path);

再或者HQL中使用left join fetch

from Category a left join fetch Category b

这里有一个奇特的情况,FetchMode枚举包含Eager和Join两个选项,但实际使用中的效果是一样的,都是输出Join语句,没有任何区别,Nhibernate如此设置,我猜想可能的原因是开始时只有Join一个选项,而后觉得不够贴切,遂增加一个Eager,但考虑到老版本兼容性,没有删除Join,所以就成了现在这个样子。下面的代码说明了在IQueryOver中如何使用预加载

q = q.Fetch(o => o.Children).Eager;

到此为止,一切都显得很完美,不过,还没完,预加载由于其生成的SQL语句包括了Join或子查询语句,因此它无法保证获取到集合中元素的唯一性,例如:A包含两个子元素B和C,那么通过预加载后,第一级查询取出的列表中会包括两个A对象,而不是通常我们想象的一个。所以,启用预加载后获取到的列表,需要手动的解决唯一性的问题,最简单的就是把列表装入ISet里“过滤”一次。

protected IList<T> ToUniqueList(IEnumerable<T> collection)
{
ISet<T> set = new HashSet<T>(collection);
return set.ToList();
}

2.3 混合加载

上面,我们只假设了Category包含子对象只有一层嵌套的情况,然而,如果子对象还有子对象,无限层嵌套时,批量加载和预加载会出现什么情况呢,首先,只采用批量加载的情况下,除第一层外,以下每层嵌套都会采用批量加载的方式,可见第一层加载的效率相对较低,其次,只采用预加载的情况下,第一次使用Join加载,获取到第一层和第二层对象,而第二层往下,每层对象的加载过程又还原到简单的Select上,与本文开头所讲的情形是一摸一样的,因此,多层次加载效率较低。那么把它们结合起来,既在映射文件中设置batch-size,又在代码中开启FetchMode.Eager,会不会综合两种的优势克服不足呢?经过实践,答案是肯定的。同时使用批量加载和预加载的情况下,首次查询时,SQL中出现了Join语句,即预加载起作用,获取到第一层和第二层对象,而后每层的查询,SQL中出现了In语句,也就是批量加载又发挥了作用,我把这种综合运用两种加载方式,结合了各自优点的新方式称为“混合加载”,这是在Nhibernate官方文档里没有的。

3.抓取策略

以上我们谈到的内容,统称为抓取策略(Fetching Strategy)。Nhibernate中,定义了一下几种抓取策略:

  • 连接抓取(Join fetching):通过 在SELECT语句使用OUTER JOIN(外连接)来获得对象的关联实例或者关联集合。
  • 查询抓取(Select fetching):另外发送一条 SELECT 语句抓取当前对象的关联实体或集合(lazy="true"时,这是默认选项)。
  • 子查询抓取(Subselect fetching):另外发送一条SELECT 语句抓取在前面查询到(或者抓取到)的所有实体对象的关联集合。(lazy="true"时)
  • 批量抓取(Batch fetching): 对查询抓取的优化方案, 通过指定一个主键或外键列表,使用单条SELECT语句获取一批对象实例或集合。

另外,Nhibernate抓取策略会区分下列各种情况:

  • Immediate fetching,立即抓取:当宿主被加载时,关联、集合或属性被立即抓取。
  • Lazy collection fetching,延迟集合抓取:直到应用程序对集合进行了一次操作时,集合才被抓取。(对集合而言这是默认行为。)
  • "Extra-lazy" collection fetching,"Extra-lazy"集合抓取:对集合类中的每个元素而言,都是直到需要时才去访问数据库。除非绝对必要,Hibernate不会试图去把整个集合都抓取到内存里来(适用于非常大的集合)。
  • Proxy fetching,代理抓取:对返回单值的关联而言,当其某个方法被调用,而非对其关键字进行get操作时才抓取。
  • "No-proxy" fetching,非代理抓取:对返回单值的关联而言,当实例变量被访问的时候进行抓取。
  • Lazy attribute fetching,属性延迟加载:对属性或返回单值的关联而言,当其实例变量被访问的时候进行抓取。需要编译期字节码强化,因此这一方法很少是必要的。

默认情况下,NHibernate对集合使用延迟select抓取,这对大多数的应用而言,都是有效的,如果需要优化这种默认策略,就需要选择适当的抓取策略,本文第二章列出的具体的可用解决方案。

4.总体原则

上文讲述了Nhibernate的抓取策略和具体解决方案,归结起来,在运用抓取策略提高性能时,总的原则就是:尽量在首次查询或每次查询时多加载关联的集合对象,在合适的地方使用抓取策略,既提高性能,又要影响其他应用场景为好。

谢谢观赏!

参考文献:

1.《NHibernate Reference Documentation 3.0》:http://nhforge.org/doc/nh/en/index.html

2.Pierre Henri Kuaté, Tobin Harris, Christian Bauer, and Gavin King.Nhibernate in Action February, 2009

时间: 2024-10-12 04:24:39

NHibernate N+1问题实例分析和优化的相关文章

Mahout机器学习平台之聚类算法详细剖析(含实例分析)

第一部分: 学习Mahout必须要知道的资料查找技能: 学会查官方帮助文档: 解压用于安装文件(mahout-distribution-0.6.tar.gz),找到如下位置,我将该文件解压到win7的G盘mahout文件夹下,路径如下所示: G:\mahout\mahout-distribution-0.6\docs 学会查源代码的注释文档: 方案一:用maven创建一个mahout的开发环境(我用的是win7,eclipse作为集成开发环境,之后在Maven Dependencies中找到相应

Android漫游记(5)---ARM GCC 内联汇编烹饪书(附实例分析)

原文链接(点击打开链接) 关于本文档 GNU C编译器针对ARM RISC处理器,提供了内联汇编支持.利用这一非常酷炫的特性,我们可以用来优化软件代码中的关键部分,或者可以使用针对特定处理的汇编处理指令. 本文假定,你已经熟悉ARM汇编语言.本文不是一篇ARM汇编教程,也不是C语言教程. GCC汇编声明 让我们从一个简单的例子开始.下面的一条ARM汇编指令,你可以添加到C源码中. /* NOP example-空操作 */ asm("mov r0,r0"); 上面的指令,讲r0寄存器的

数据挖掘技术在信用卡业务中的应用及实例分析

信用卡业务具有透支笔数巨大.单笔金额小的特点,这使得数据挖掘技术在信用卡业务中的应用成为必然.国外信用卡发卡机构已经广泛应用数据挖掘技术促进信用卡业务的发展,实现全面的绩效管理.我国自1985年发行第一张信用卡以来,信用卡业务得到了长足的发展,积累了巨量的数据,数据挖掘在信用卡业务中的重要性日益显现. 一.数据挖掘技术在信用卡业务中的应用 数据挖掘技术在信用卡业务中的应用主要有分析型客户关系管理.风险管理和运营管理. 1.分析型CRM 分析型CRM应用包括市场细分.客户获取.交叉销售和客户流失.

C#验证码识别基础方法实例分析

本文实例讲述了C#验证码识别基础方法,是非常实用的技巧.分享给大家供大家参考.具体方法分析如下: 背景 最近有朋友在搞一个东西,已经做的挺不错了,最后想再完美一点,于是乎就提议把这种验证码给K.O.了,于是乎就K.O.了这个验证码.达到单个图片识别时间小于200ms,500个样本人工统计正确率为95%.由于本人没有相关经验,是摸着石头过河.本着经验分享的精神,分享一下整个分析的思路.在各位大神面前献丑了. 再来看看部分识别结果如下图所示: 这里是不是看着很眼熟?下面再来具体分析一下. 处理第一步

总结:windows下性能分析以及优化报告

性能分析以及优化     使用的是vs2017自带的性能分析工具. 主要分析了遇到的性能瓶颈,以及想到的优化方法,有的验证了,有的没有来得及. 首先看整体用时以及cpu占有率. 最终在我的设备上(I5-5200U 三星860EVO固态)运行时间约为27.3S.期间cpu占有率比较稳定. 前0.5秒cpu占用率低,大概是因为这段时间是刚开始读取文件,cpu并没有处理任务,后来便进入一边读取一遍计算的状态,cpu占有率就上来了,大概25%,但是还是不高. 而且在这里我遇到一个十分奇怪的现象 直到代码

实现 | 朴素贝叶斯模型算法研究与实例分析

实现 | 朴素贝叶斯模型算法研究与实例分析(白宁超2018年9月4日09:03:21) 导读:朴素贝叶斯模型是机器学习常用的模型算法之一,其在文本分类方面简单易行,且取得不错的分类效果.所以很受欢迎,对于朴素贝叶斯的学习,本文首先介绍理论知识即朴素贝叶斯相关概念和公式推导,为了加深理解,采用一个维基百科上面性别分类例子进行形式化描述.然后通过编程实现朴素贝叶斯分类算法,并在屏蔽社区言论.垃圾邮件.个人广告中获取区域倾向等几个方面进行应用,包括创建数据集.数据预处理.词集模型和词袋模型.朴素贝叶斯

分享一个基于小米 soar 的开源 sql 分析与优化的 WEB 图形化工具

soar-web 基于小米 soar 的开源 sql 分析与优化的 WEB 图形化工具,支持 soar 配置的添加.修改.复制,多配置切换,配置的导出.导入与导入功能. 环境需求 python3.xFlaskpymysql Python 环境未安装的可参考下面操作: Windows:step 1 去 python 官网下载安装 python3 (已安装可跳过此步骤)setp 2 pip install Flasksetp 3 pip install pymysql Mac:step 1 brew

【OpenGL】Shader实例分析(七)- 雪花飘落效果

转发请保持地址:http://blog.csdn.net/stalendp/article/details/40624603 研究了一个雪花飘落效果.感觉挺不错的.分享给大家,效果例如以下: 代码例如以下: Shader "shadertoy/Flakes" { // https://www.shadertoy.com/view/4d2Xzc Properties{ iMouse ("Mouse Pos", Vector) = (100,100,0,0) iChan

Apache漏洞利用与安全加固实例分析

Apache 作为Web应用的载体,一旦出现安全问题,那么运行在其上的Web应用的安全也无法得到保障,所以,研究Apache的漏洞与安全性非常有意义.本文将结合实例来谈谈针对Apache的漏洞利用和安全加固措施. Apache HTTP Server(以下简称Apache)是Apache软件基金会的一个开放源码的网页服务器,可以在大多数计算机操作系统中运行,是最流行的Web服务器软件之一.虽然近年来Nginx和Lighttpd等Web Server的市场份额增长得很快,但Apache仍然是这个领