`

Hibernate N+1 问题

 
阅读更多

Hibernate3中取得多层数据的所产生的n+1 selects问题的解决.

其实这个问题在Hibernate in Action中已经有很多种解决办法了。但我觉得其中最好的办法是用Criteria的FetchMode来解决,但是Hibernate in Action中写的很不详细。我昨晚试了好长时间来的到答案。下面总结一下。

需求这样的,我有四张表(one,two,three,four)从one一直外键关联到four。结构如下:

现在在Session中得到One,并从One里一直取到Four里的内容。如果简单的用Session.get来实现是这样的。

One one = (One)session.get(One.class,new Integer(1));
        Iterator iterone = one.getTwos().iterator();
        while(iterone.hasNext()){
            Two two = (Two) iterone.next();
            Iterator itertwo = two.getThrees().iterator();
            while(itertwo.hasNext()){
                Three three = (Three) itertwo.next();
                three.getFours().size();                
            }
        }

这样我在Session关闭后返回的One里是从One到Four的信息都有的。
然而这样做所导致的结果是生成大量的SQL查询,这是一个典型的n+1 Selects问题。如果系统结构层次多,符合条件的记录多,那么Hibernate为你生成的SQL查询将是难以接受的。
对于这个例子生成的SQL是这样的

Hibernate: select one0_.c_one_id as c1_0_, one0_.c_one_text as c2_3_0_ from Oneone0_ where one0_.c_one_id=?
Hibernate: select twos0_.c_one_id as c2_1_, twos0_.c_two_id as c1_1_,twos0_.c_two_id as c1_0_, twos0_.c_one_id as c2_2_0_, twos0_.c_two_text asc3_2_0_ from Two twos0_ where twos0_.c_one_id=?
Hibernate: select threes0_.c_two_id as c2_1_, threes0_.c_three_id as c1_1_,threes0_.c_three_id as c1_0_, threes0_.c_two_id as c2_1_0_,threes0_.c_three_text as c3_1_0_ from Three threes0_ where threes0_.c_two_id=?
Hibernate: select fours0_.c_three_id as c2_1_, fours0_.c_four_id as c1_1_,fours0_.c_four_id as c1_0_, fours0_.c_three_id as c2_0_0_, fours0_.c_four_textas c3_0_0_ from Four fours0_ where fours0_.c_three_id=?
Hibernate: select fours0_.c_three_id as c2_1_, fours0_.c_four_id as c1_1_,fours0_.c_four_id as c1_0_, fours0_.c_three_id as c2_0_0_, fours0_.c_four_textas c3_0_0_ from Four fours0_ where fours0_.c_three_id=?
Hibernate: select threes0_.c_two_id as c2_1_, threes0_.c_three_id as c1_1_,threes0_.c_three_id as c1_0_, threes0_.c_two_id as c2_1_0_,threes0_.c_three_text as c3_1_0_ from Three threes0_ where threes0_.c_two_id=?
Hibernate: select fours0_.c_three_id as c2_1_, fours0_.c_four_id as c1_1_,fours0_.c_four_id as c1_0_, fours0_.c_three_id as c2_0_0_, fours0_.c_four_textas c3_0_0_ from Four fours0_ where fours0_.c_three_id=?
Hibernate: select fours0_.c_three_id as c2_1_, fours0_.c_four_id as c1_1_,fours0_.c_four_id as c1_0_, fours0_.c_three_id as c2_0_0_, fours0_.c_four_textas c3_0_0_ from Four fours0_ where fours0_.c_three_id=?

 

对于这样的问题,在没有Hibernate以前我们一般都用jdbc来做,那样的话我们其实用一个进行3次join的sql语句就可以实现,但是这样解决也有问题,就是返回的ResultSet中的数据非常多,而且杂乱,其实是从one到four平行排列的。对于这样的结果集我们要把它手动影射曾对象结构也是一个很复杂的操作。
幸好Hibernate3可以为我们做这些事情(我再一次被Hibernate的强大所震撼)

上面的实现可以用Criteria来实现:

session = sessionFactory.openSession();
        Criteria criteria = session.createCriteria(One.class);
        criteria.add(Expression.eq("COneId",new Integer(1)));
        one = (One)criteria.setFetchMode("twos",FetchMode.JOIN).setFetchMode("twos.threes",FetchMode.JOIN).setFetchMode("twos.threes.fours",FetchMode.JOIN).uniqueResult();
        session.close();

这里的重点是这句话criteria.setFetchMode("twos",FetchMode.JOIN).setFetchMode("twos.threes",FetchMode.JOIN).setFetchMode("twos.threes.fours",FetchMode.JOIN).uniqueResult();
在用Criteria之前先设置FetchMode,应为Criteria是动态生成sql语句的,所以生成的sql就是一层层Join下去的。
setFetchMode(String,Mode)第一个参数是association path,用"."来表示路径。这一点具体的例子很少,文档也没有写清楚。我也是试了很久才试出来的。
就这个例子来所把因为取道第四层,所以要进行三次setFetchMode
第一次的路径是twos,一位one中有two的Set。这个具体要更具hbm.xml的配置来定。
第二个路径就是twos.threes
第三个就是twos.threes.fours
一次类推,一层层增加的。
这样做法最终生成的SQL是这样的:

Hibernate: select this_.c_one_id as c1_3_, this_.c_one_text as c2_3_3_,twos2_.c_one_id as c2_5_, twos2_.c_two_id as c1_5_, twos2_.c_two_id as c1_0_,twos2_.c_one_id as c2_2_0_, twos2_.c_two_text as c3_2_0_, threes3_.c_two_id asc2_6_, threes3_.c_three_id as c1_6_, threes3_.c_three_id as c1_1_,threes3_.c_two_id as c2_1_1_, threes3_.c_three_text as c3_1_1_,fours4_.c_three_id as c2_7_, fours4_.c_four_id as c1_7_, fours4_.c_four_id asc1_2_, fours4_.c_three_id as c2_0_2_, fours4_.c_four_text as c3_0_2_ from Onethis_ left outer join Two twos2_ on this_.c_one_id=twos2_.c_one_id left outerjoin Three threes3_ on twos2_.c_two_id=threes3_.c_two_id left outer join Fourfours4_ on threes3_.c_three_id=fours4_.c_three_id where this_.c_one_id=?

 虽然很长但是只有一条SQL语句。性能要好很多。Hibernate的强大之处是它会把返回的ResultSet自动影射到你的对象模型里面去。这就为我们省了很多事。

看来Hibernate真是一个耐人寻味的Framework啊

Hibernate N+1 问题及解决办法

问题出现的原因:

Hibernate 中常会用到 set , bag 等集合表示 1 对多的关系,在获取实体的时候就能根据关系将关联的对象或者对象集取出,还可以设定 cacade 进行关联更新和删除。这不得不说 hibernate 的 orm 做得很好,很贴近 oo 的使用习惯了。

但是对数据库访问还是必须考虑性能问题的,在设定了 1 对多这种关系之后, 查询就会出现传说中的 n+1 问题。

一对多:在一方,查找得到了 n 个对象,那么又需要将 n 个对象关联的集合取出,于是本来的一条 sql 查询变成了 n+1 条;

多对一:在多方,查询得到了 m 个对象,那么也会将 m 个对象对应的 1 方的对象取出, 也变成了 m+1 ;

解决问题的方法:

1、 使用 fetch 抓取, Hibernate 抓取策略分为单端代理和集合代理的抓取策略。

Hibernate 抓取策略 ( 单端代理的抓取策略) :

保持默认也就是如下 :

<many-to-one name="clazz"cascade="save-update" fetch="select" />

fetch="select" 就是另外发送一条 select 语句抓取当前对象关联实体或者集合设置 fetch="join"

<many-to-one name="clazz"cascade="save-update" fetch="join"/>

Hibernate 会通过 select 语句使用外连接来加载器关联实体活集合此时 lazy 会失效

Hibernate 抓取策略 ( 集合代理的抓取策略 ) :

保持默认( fetch="select" )也就是如下 :

<set name="students"inverse="true">

<key column="clazz"/>

<one-to-manyclass="com.june.hibernate.Student"/>

</set>

1)fetch="select" 会另外发出一条语句查询集合

2) 设置fetch="join" 采用外连接集合的 lazy 失效

3) 这只fetch="subselect" 另外发出一条 select 语句抓取前面查询到的所有的实体对象的关联集合 fetch 只对 HQL 查询产生影响其他的则不会

2、 使用 map 直接搜索需要的列

如:产品 product 和产品分类 product_category 两张表,多对一关系。查询产品列表时

select new Map(p.id as id, p.name as name,p.category.name as categoryName) from Product p

Hibernate中常会用到set,bag等集合表示1对多的关系,在获取实体的时候就能根据关系将关联的对象或者对象集取出, 还可以设定cacade进行关联更新和删除。这不得部说hibernate的orm做得很好,很贴近oo的使用习惯了。
但是对数据库访问还是必须考虑性能问题的, 在设定了1对多这种关系之后,查询就会出现传说中的n+1问题。
1)1对多,在1方,查找得到了n个对象, 那么又需要将n个对象关联的集合取出,于是本来的一条sql查询变成了n+1条
2)多对1,在多方,查询得到了m个对象,那么也会将m个对象对应的1方的对象取出, 也变成了m+1

怎么解决n+1问题?
1)lazy=true, hibernate3开始已经默认是lazy=true了;lazy=true时不会立刻查询关联对象,只有当需要关联对象(访问其属性,非id字段)时才会发生查询动作。
2)二级缓存, 在对象更新,删除,添加相对于查询要少得多时,二级缓存的应用将不怕n+1问题,因为即使第一次查询很慢,之后直接缓存命中也是很快的。
不同解决方法,不同的思路,第二条却刚好又利用了n+1。

  • 大小: 31.9 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics