什么叫n+1次select查询问题?
选自<<精通Hibernate:Java对象持久化技术详解>> 作者:
孙卫琴
来源:www.javathinker.org
如果转载,请标明出处,谢谢
在Session的缓存中存放的是相互关联的对象图。默认情况下,当Hibernate从数据库中加载Customer对象时,会同时加载所有关联的Order对象。以Customer和Order类为例,假定ORDERS表的CUSTOMER_ID外键允许为null,图1列出了CUSTOMERS表和ORDERS表中的记录。
以下Session的find()方法用于到数据库中检索所有的Customer对象:
List customerLists=session.find(“from Customer as c”);
运行以上find()方法时,Hibernate将先查询CUSTOMERS表中所有的记录,然后根据每条记录的ID,到ORDERS表中查询有参照关系的记录,Hibernate将依次执行以下select语句:
select * from CUSTOMERS;
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;
通过以上5条select语句,Hibernate最后加载了4个Customer对象和5个Order对象,在内存中形成了一幅关联的对象图,参见图2。
Hibernate在检索与Customer关联的Order对象时,使用了默认的立即检索策略。这种检索策略存在两大不足:
(1) select语句的数目太多,需要频繁的访问数据库,会影响检索性能。如果需要查询n个Customer对象,那么必须执行n+1次select查询语句。
这就是经典的n+1次select查询问题。
这种检索策略没有利用SQL的连接查询功能,例如以上5条select语句完全可以通过以下1条select语句来完成:
select * from CUSTOMERS left outer join ORDERS
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID
以上select语句使用了SQL的左外连接查询功能,能够在一条select语句中查询出CUSTOMERS表的所有记录,以及匹配的ORDERS表的记录。
(2)在应用逻辑只需要访问Customer对象,而不需要访问Order对象的场合,加载Order对象完全是多余的操作,这些多余的Order对象白白浪费了许多内存空间。
为了解决以上问题,Hibernate提供了其他两种检索策略:延迟检索策略和迫切左外连接检索策略。延迟检索策略能避免多余加载应用程序不需要访问的关联对象,迫切左外连接检索策略则充分利用了SQL的外连接查询功能,能够减少select语句的数目。
==============================================
1、outer-
join
关键字(many-to-one的情况)
outer-
join
关键字有3个值,分别是true,false,auto,默认是auto。
true: 表示使用外连接抓取关联的内容,这里的意思是当使用load(OrderLineItem.class,”id”)时,
Hibernate
只生成一条SQL语句将OrderLineItem与他的父亲Order全部初始化。
select * from OrderLineItem o left
join
Order p on o.OrderId=p.OrderId where o.OrderLineItem_Id=?
false:表示不使用外连接抓取关联的内容,当load(OrderLineItem.class,”id”)时,
Hibernate
生成两条SQL语句,一条查询OrderLineItem表,另一条查询Order表。这样的好处是可以设置延迟加载,此处要将Order类设置为lazy=true。
select * from OrderLineItem o where o.OrderLineItem_Id=?
select * from Order p where p.OrderId=?
auto:具体是ture还是false看
hibernate
.cfg.xml中的配置
注意:如果使用HQL查询OrderLineItem,如 from OrderLineItem o where o.id=’id’,总是不使用外部抓取,及outer-
join
失效。
2、outer-
join
(集合)
由于集合可以设置lazy=”true”,所以lazy与outer-
join
不能同时为true,当lazy=”true”时,outer-
join
将一直是false,如果lazy=”false”,则outer-
join
用法与1同
3、HQL语句会将POJO配置文件中的关联一并查询,即使在HQL语句中没有明确
join
。
and then do
MyObject obj = session.createQuery("from MyObject").uniqueResult(); obj.getMySet().iterator().next();
you will still have an additional query and no outer-
join
. So you must explicily request the outer-
join
fetching:
MyObject obj = session.createQuery( "from MyObject mo left join fetch mo.mySet").uniqueResult(); obj.getMySet().iterator().next();
Another important thing to know is that you can only
fetch
one collection reference in a query. That means you can just use one
fetch
join
. You can however
fetch
“one” references in addition, as this sample from the
Hibernate
Docs demonstrates:
from eg.Cat as cat inner join fetch cat.mate left join fetch cat.kittens
We have once considered lifting this limitation, but then decided against it, because using more than one
fetch
–
join
would be a bad idea generally: The generated ResultSet becomes huge and is a major performance loss.
So alltogether the ”
fetch
join
” clause is an important instrument
Hibernate
users should learn how to leverage, as it allows tuning the
fetch
behaviour of a certain use case.
5、
join
fetch
与
join
的区别
如果HQL使用了连接,但是没有使用
fetch
关键字,则生成的SQL语句虽然有连接,但是并没有取连接表的数据,还是需要单独的sql取数据,也就是 select a,b,d…中没有连接表的字段
6、如果集合被声明为lazy=true,在HQL中如果显式的使用
join
fetch
则延迟加载失效。
7、在one-to-many的one端显式设置fecth=”join”,则无论如何都采取预先抓取(生成一个SQl),延迟加载失效(生成两个SQL)
8、many-to-one的延迟加载是在配置文件的class标签设置lazy=”true”,one-to-many和many-to-many的延迟加载是在set标签中设置lazy=”true”。而one-to-one不只要在calss标签设置lazy=”true”,而且要在one-to-one标签中设置constrained=”true”.
===============================
实这个问题在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 One one0_ 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 as c3_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_text as 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_text as 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_text as 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_text as 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 as c2_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 as c1_2_, fours4_.c_three_id as c2_0_2_, fours4_.c_four_text as c3_0_2_ from One this_ left outer join Two twos2_ on this_.c_one_id=twos2_.c_one_id left outer join Three threes3_ on twos2_.c_two_id=threes3_.c_two_id left outer join Four fours4_ on threes3_.c_three_id=fours4_.c_three_id where this_.c_one_id=?
虽然很长但是只有一条SQL语句。性能要好很多。Hibernate的强大之处是它会把返回的ResultSet自动影射到你的对象模型里面去。这就为我们省了很多事。