论坛首页 Java版 Hibernate

鉴于反复出现讨论hibernate适用性问题的帖子,这次希望有个定论

浏览 33075 次
该帖已经被评为良好帖
作者 正文
时间:2007-12-27
firmgoal 写道

Resume表与几个子表确实是1:N关系。是这样,由于应聘区域、应聘行业、应聘职位等等,不仅仅是与字典表的关系,而且还存在关系属性,而且针对于不同子表的关系属性还不一样,因此这个中间关系最后形成了实体,即Resume是一张表,ResumeApplyArea是一张表,ResumeApply...等等。比如应聘区域部分,还需要有是否只包括本区域,及本区域及以下一级区域,等等。本来类似于这些东西,可以从字典代码本身的设计中体现出来,只不过由于是升级项目,字典代码原来设计得就不系统,所以需要在关系中添加属性来解决这些问题。

即使是这样,原来也考虑过把这些子表做到一张表里,多余的字段都放在这张表里,后来考虑了两个问题:
1、字典外键。字典不是所有的字典都在一张表中,因此如果要从数据库级别做到外键约束,保证字典代码不会有查不到的情况,把子表做到一张表中,是实现不了的。
2、原来起初没有统计和查询的要求,如果仅仅是录入,显示,实际上分开表来做,速度还会快一些。

因此,最后没有把这些子表做到一张表中。
另外,即使这些子表放到一张表中,关联查询的效率也是不能忍受的,因为要查询出来的学生信息分布在三个表中,User表,比如登录名之流,Student表,比如学生学校等,Resume表,比如自我简介等,130×120×300×XXXX(万)......


看起来是因为模型设计不好导致的查询性能问题。

1、User和Student可以装进一张表里面,这是ORM的一种标准映射模型,把整个继承树映射到一张表;
2、应聘区域,行业,职位这些信息不构成对象关联关系,不应该让Resume建立到这些字典表的映射,这些信息实际上只是Resume的属性而已,不是对象关联,应该直接保存到Resume表的字段。

你的查询其实就是User表和Resume表的两张表外键关联查询,查询条件当中的应聘区域,行业,职位都应该建立索引,所以这条查询即使是上百万的大表关联查询,实际上数据库需要扫描的记录行数并不会很多,如果explain一下的话,估计先扫描Resume表的上千条记录(有三个建立索引的where条件约束过了,某个区域的某个行业的某个职位应聘简历撑死了上千),再扫描User表相应的上千条记录,查询应该在0.1秒以内完成。
   
1 请登录后投票
时间:2007-12-27

robbin,谢谢你的回复。

其实我这样问你也是想给自己多找一些使用hibernate的自信心。

就我自己使用hb的经验来看,其实我也认同很多性能问题其实都是不洽当的使用hb或者错误的数据库设计、错误的编程模式造成的。比如:在我自己负责的多个项目中就发现很多程序员喜欢在循环中去做一些查库操作;或者条件查询时根本就不使用分页查询;或者在不知道什么是lazyload以及get方法会引起查库操作等等的情况下。就在循环当中去调用get方法。

说实话:我在建行的很多j2ee项目中【包括建总行级的项目】都发现了以上的编程模式造成的性能问题,并且自己也亲自去解决优化过这些问题。

但是很多银行的所谓“高人”们在一遇到这种问题时往往会做出类似“看看,java性能确实不好”、“hb有性能问题”这样的结论。

其实我只是觉得非常的好笑。采用错误的编程模式,不管你用hb还是jdbc还是ibatis都会有性能问题的!!

robbin,我觉得你这句话说得非常好:“采不采用hibernate只是一个战术问题”

最后我自己总结一句话:国内很多公司的技术领导,一旦在走向“领导”的岗位后就忘记了实践出真知的道理,喜欢以自己的“权威”来做结论。我想这也可能是中国的it技术始终是跟在别人屁股后面的其中一个原因吧!不知道大家是否有同感!
   
0 请登录后投票
时间:2007-12-27
robbin 写道
firmgoal 写道

Resume表与几个子表确实是1:N关系。是这样,由于应聘区域、应聘行业、应聘职位等等,不仅仅是与字典表的关系,而且还存在关系属性,而且针对于不同子表的关系属性还不一样,因此这个中间关系最后形成了实体,即Resume是一张表,ResumeApplyArea是一张表,ResumeApply...等等。比如应聘区域部分,还需要有是否只包括本区域,及本区域及以下一级区域,等等。本来类似于这些东西,可以从字典代码本身的设计中体现出来,只不过由于是升级项目,字典代码原来设计得就不系统,所以需要在关系中添加属性来解决这些问题。

即使是这样,原来也考虑过把这些子表做到一张表里,多余的字段都放在这张表里,后来考虑了两个问题:
1、字典外键。字典不是所有的字典都在一张表中,因此如果要从数据库级别做到外键约束,保证字典代码不会有查不到的情况,把子表做到一张表中,是实现不了的。
2、原来起初没有统计和查询的要求,如果仅仅是录入,显示,实际上分开表来做,速度还会快一些。

因此,最后没有把这些子表做到一张表中。
另外,即使这些子表放到一张表中,关联查询的效率也是不能忍受的,因为要查询出来的学生信息分布在三个表中,User表,比如登录名之流,Student表,比如学生学校等,Resume表,比如自我简介等,130×120×300×XXXX(万)......


看起来是因为模型设计不好导致的查询性能问题。

1、User和Student可以装进一张表里面,这是ORM的一种标准映射模型,把整个继承树映射到一张表;
2、应聘区域,行业,职位这些信息不构成对象关联关系,不应该让Resume建立到这些字典表的映射,这些信息实际上只是Resume的属性而已,不是对象关联,应该直接保存到Resume表的字段。

你的查询其实就是User表和Resume表的两张表外键关联查询,查询条件当中的应聘区域,行业,职位都应该建立索引,所以这条查询即使是上百万的大表关联查询,实际上数据库需要扫描的记录行数并不会很多,如果explain一下的话,估计先扫描Resume表的上千条记录(有三个建立索引的where条件约束过了,某个区域的某个行业的某个职位应聘简历撑死了上千),再扫描User表相应的上千条记录,查询应该在0.1秒以内完成。



算我多想了。我一直以为,一份resume怎么就不能投多个职位呢?既然这么简单处理了,那很好办,照robbin说的,作为resume的属性。


另外,不赞同robbin把User和Student塞进一张表。user今年是student,明年就变成worker了。
   
0 请登录后投票
时间:2007-12-27
sorphi 写道
另外,不赞同robbin把User和Student塞进一张表。user今年是student,明年就变成worker了。

可否考虑把通用的属性放到User中,把学生特有的属性放到Student中?User与Student之间建立1:0..1的关联,User与Worker的关联关系用类似的办法处理?
   
0 请登录后投票
时间:2007-12-27
引用
就我自己使用hb的经验来看,其实我也认同很多性能问题其实都是不洽当的使用hb或者错误的数据库设计、错误的编程模式造成的。比如:在我自己负责的多个项目中就发现很多程序员喜欢在循环中去做一些查库操作;或者条件查询时根本就不使用分页查询;或者在不知道什么是lazyload以及get方法会引起查库操作等等的情况下。就在循环当中去调用get方法。

说实话:我在建行的很多j2ee项目中【包括建总行级的项目】都发现了以上的编程模式造成的性能问题,并且自己也亲自去解决优化过这些问题。

但是很多银行的所谓“高人”们在一遇到这种问题时往往会做出类似“看看,java性能确实不好”、“hb有性能问题”这样的结论。

其实我只是觉得非常的好笑。采用错误的编程模式,不管你用hb还是jdbc还是ibatis都会有性能问题的!!


是的,任何编程方法都会有自己的陷阱,讥刺Java性能不好,那C++程序员写的代码弄不好就把操作系统搞crash的又怎么说?

RoR的ActiveRecord照样有陷阱,他的1:n关系当中的_count魔法字段当不使用counter_cache声明的时候,在update操作当中会带来致命的性能问题。比方说topics表当中有一个posts_count字段,而topic和post有1:n的关系声明,那当程序员手工更新posts_count字段的时候,ActiveRecord就发送一条: select * from topics的SQL,把整个topics表全部抓出来,可怕吧!

程序员编程自己不注意代码质量,就是不用ORM,光用JDBC,难道就写不出来烂代码?难道Hibernate没有问世之前,Java程序员从来就不会碰到访问数据库的性能瓶颈吗?我们2000年的时候写的JDBC烂代码不使用榜定变量,导致Oracle数据库每周都要crash一次。
   
0 请登录后投票
时间:2007-12-27
sorphi 写道

算我多想了。我一直以为,一份resume怎么就不能投多个职位呢?既然这么简单处理了,那很好办,照robbin说的,作为resume的属性。


另外,不赞同robbin把User和Student塞进一张表。user今年是student,明年就变成worker了。


其实即便建立resume和应聘区域,应聘公司,应聘职位的外键关联,其实不会导致性能问题。比方说,resume表有一个applyRegionId字段,关联应聘区域字典表,有一个applyCompanyId字典,关联应聘公司字典表,有一个applyJobPositionId字段,关联应聘职位字典表。

那你查询的时候,必然首先获得应聘区域,应聘公司和应聘职位三个条件,那构造where条件的时候,比方说HQL:

where applyRegion = :region AND applyCompany = :company AND applyJobPosition = :jobPosition


实际上生成的SQL查询where条件就是:

where applyRegionId = 123 AND applyCompanyId = 213 AND applyJobPositionId = 321


查询参数代入分别是字典表的主键值,哪里有什么关联字典表的关联查询?

更进一步,即使User和Student表分别是两张表,也不会关联User表去查询。实际上你要写的查询语句就是:

select * from student right join resume on student.resumeId = resume.id where resume.applyRegionId = XXX and resume.applyCompanyId = XXX......


还是两张表的关联查询,考虑到where条件是外键,而且被索引了,这条SQL也就是不到0.1秒就完成了。查询出来的students在页面遍历显示的时候,会发送n条SQL查询User表:

select * from user where id = XXX;
select * from user where id = XXX;
...
select * from user where id = XXX;


但是这n条可都是主键查询阿,速度非常快,而且当你使用了对象缓存以后,根本就不需要查询数据库,统统从Cache Server当中获得。性能怎么会不好?
   
0 请登录后投票
时间:2007-12-27
robbin, 大哥!咱跑题跑到给firmgoal的具体案例来做解决方案了。罪过罪过...
robbin 写道

resume表有一个applyRegionId字段,关联应聘区域字典表


你这个只能应付简单的多对一关联啊。我想的是多对多,即resume.applyRegions,resume.applyJobPositions,...
换做我的解决方法是:resume.applyXXXXs
之前给出过sql
select
  r.*
from  
  resume r  
where  
  exists (  
    select rd.resume_id   
    from  
      resume_dict_rel rd  
    where  
      rd.dict_id in (:industry_id,:region_id,:position_id,...)  
      and rd.resume_id=r.id  
    group by rd.resume_id  
    having count(*)=:dict_count  
  )
  and ...


准备实体图:
HB.initialize(resume.getApplyXXXXs()); //maybe hit secondary cache
HB.initialize(resume.getPerson()); //maybe hit secondary cache


如果我来做,我会把student, worker, ...,作为基于person接口的隐性多态了,所以resume里面关联的应该是person。


不过,这些都是纸上谈兵。站在第一线的firmgoal说了,他们的region还是树形结构!咱谈的这些都不符合他们的实际情况,所以,谁做谁知道。
   
0 请登录后投票
时间:2007-12-27
robbin 写道

看起来是因为模型设计不好导致的查询性能问题。

1、User和Student可以装进一张表里面,这是ORM的一种标准映射模型,把整个继承树映射到一张表;
2、应聘区域,行业,职位这些信息不构成对象关联关系,不应该让Resume建立到这些字典表的映射,这些信息实际上只是Resume的属性而已,不是对象关联,应该直接保存到Resume表的字段。

你的查询其实就是User表和Resume表的两张表外键关联查询,查询条件当中的应聘区域,行业,职位都应该建立索引,所以这条查询即使是上百万的大表关联查询,实际上数据库需要扫描的记录行数并不会很多,如果explain一下的话,估计先扫描Resume表的上千条记录(有三个建立索引的where条件约束过了,某个区域的某个行业的某个职位应聘简历撑死了上千),再扫描User表相应的上千条记录,查询应该在0.1秒以内完成。



1、考虑过User和Student放一张表里,一张表最麻烦的就是NOT NULL,而且Student的属性有30来个,而且还有三四种用户类型,而且要考虑有新用户类型的加入,SO,最后没放到一张表,把整个继承树映射到了多张表中。现在的解决方案,还是分表,只不过,把User的部分属性通过触发器同步到Student表中。就相当于通过数据冗余放到一张表了。
2、就像sorphi说的,每个人不是只有一个应聘区域、行业,可以应聘多个区域、行业。是一对N的关系,而且关系带关系属性,不能放到Resume中作为字段存在。

关于我举的这个案例,就讨论这么多吧,根据实际需求来,解决方案可能会有很多,最终的根据实际上还是Robbin的“磁盘IO”的说法。我实际上只是想通过这个案例来说明,范式理论了一些,冗余必不可少。
   
0 请登录后投票
时间:2007-12-27
另外,即使像Robbin说的,只有Student表和Resume表,我测试了一下,也是堪忧:

select * from student s join resume r on s.guid=r.stu_guid where r.resumetime > to_date('2007-12-01','yyyy-mm-dd')

已选择86232行。

已用时间: 00: 02: 24.09

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=1072 Card=3371 Bytes
=3084465)

1 0 NESTED LOOPS (Cost=1072 Card=3371 Bytes=3084465)
2 1 TABLE ACCESS (BY INDEX ROWID) OF 'RESUME' (Cost=398 Card
=3371 Bytes=384294)

3 2 INDEX (RANGE SCAN) OF 'RESUME_RESUMETIME_IDX' (NON-UNI
QUE) (Cost=24 Card=3371)

4 1 TABLE ACCESS (BY INDEX ROWID) OF 'STUDENT_BASE_INFO' (Co
st=2 Card=1 Bytes=801)

5 4 INDEX (UNIQUE SCAN) OF 'SYS_C0013212' (UNIQUE)




Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
539218 consistent gets
8155 physical reads
0 redo size
74740354 bytes sent via SQL*Net to client
603900 bytes received via SQL*Net from client
86234 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
86232 rows processed
   
0 请登录后投票
时间:2007-12-27
一个人投简历,按照上面的场景,假设允许他多次投递,那么他的每次投递目标是(地区1,行业2,职位3)的组合,而不是单独分别对三个条件独立的投递,先不考虑投递目标还附带条件的情况,那么这个投递目标或者说投递条件可以有两种存储方式:

一、建立对象映射

ResumeApply表保存resumeId, regionId, industryId, positionId,而Resume和ResumeApply是1:n的关系,查询语句如下:

select * from student left join resume on student.id = resume.userId left join resumeApply on resume.id = resumeApply.resumeId where resumeApply.regionId = ? AND ......

这样三张表关联查询,就可以得到Student了。考虑一下where条件的约束情况,resumeApply可能需要扫描上千条(同一个职位有上千个人投递),但是resumeApply字段很少,即便表记录很多,扫描速度也会很快,然后Resume也相应扫描上千条,Student表也是一样。算起来,我估计查询时间不会超过1秒钟。

二、简历的投递也可以不建立对象关联,而是把投递情况保存到一个字段里面

比方说Resume表有一个apply的字段,类型是text的,投递存储格式如下:

r=1,i=2,p=3|r=3,i=1,p=5| ......

表明该简历投递了两次,分别投递到(区域1,行业2,职位3),(区域3,行业1,职位5),如果投递简历还有附加信息,还可以把附件信息也放进去,例如

r=1_r,i=2,p=3|r=3,i=1,p=5| ......,表达投递区域1的时候投递子区域。

这样查询语句就是:
select * from student left join resume on student.id=resume.userId where locate(?, resume.apply) <> 0

需要自己构造同样投递格式的条件参数,然后用数据库的字符串函数运算,不算复杂,但是难以避免对resume表的全表扫描了。估计速度比方案一会慢很多,如果resume表有上百万,可能要5-6秒钟时间。但是好处是你可以在投递字段里面放更多查询信息。
   
0 请登录后投票
论坛首页 Java版 Hibernate

跳转论坛:
JavaEye推荐