论坛首页 Java企业应用论坛

Hibernate 3.2中annotation注释的位置对性能的巨大影响

浏览 7357 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2008-07-07   最后修改:2008-12-18
标题写的有点别扭,此问题说来话长,慢慢道来:

近日把自己的AppFuse改写成JDK 1.5的形式,主要的改动是Hibernate配置改成annotation配置,原本认为很顺利,没想到碰到很多怪问题,不得不多次拿Hibernate源码进行跟踪。

其中一个要命的问题如下:

原本我是用xDoclet2的方式写注释,运行ant生成hbm配置脚本,xDoclet1一定要写在getMethod上,让我很不爽,xDoclet2注释可以写在field上,代码可读性不错,因为field的语义注释一般都是写在field上的,不大会写在getMethod上,不过xDoclet2跑在JDK 1.4下会报错,不过还好不影响生成。

说了这么多废话,其实就是表明原来的方式是运行在hbm配置文件的方式,希望改写成annotation后原有的数据抓取表现行为一致。

先说说JDK 1.4下的情况:
运行环境Hibernate 3.2多对一生成的配置举例,假设是user.hbm.xml(test.User):
<many-to-one column="group_id" name="group" class="test.Group"/>
在这个配置的默认情况下,分析如下代码:
User user = userManager.findByName("diablo3");
Group group = user.getGroup();
group.getGroupId();
group.getName();

首先第二行不会产生select ... from group WHERE group_id=? 的代码。
关键的是第三行也不会产生,直到第四行才会触发这条sql。
这里还有奇怪的现象,如果在Group.java中有如下代码:
public String getGroupIdTest01() {
    return groupId;
}
public String getGroupIdTest02() {
    return this.getGroupId();
}

然后同等条件执行user.getGroupIdTest01()和user.getGroupIdTest02()都会触发sql语句的生成,昏倒啊,暂且作伏笔,先不讨论这个诡异现象。

先介绍这个不触发sql的用途和应用场景。
比如在一个页面上要显示用户列表(不管是写页面方式,还是JSON序列化都一样),最后一列有一个按钮,按钮上的文字是“显示所属小组详细信息”,那这个按钮触发的事件可能是个弹出窗口或者URL跳转,一般要带个参数groupId=xxx来定位要显示的小组ID,此时数据抓取深度显然不需要group的其他信息,如果触发<script type="text/javascript" src="http://www.iteye.com/javascripts/tinymce/themes/advanced/langs/zh.js"></script><script type="text/javascript" src="http://www.iteye.com/javascripts/tinymce/plugins/javaeye/langs/zh.js"></script>上面提到的sql,势必产生N+1的性能问题(这也是标题中“巨大”的由来),其实如果事先知道要小组的其它信息,这个页面显示的做法就完全是其他策略了。也就是说,确保调用user.getGroupId()不会产生新的sql是很重要的,能减少很多麻烦。


然后说说JDK 1.5下,当我写新的例子:
@Id
@Column(name = "group_id")
private String groupId;

我为了确保原先的策略不会有问题,特地又做了个实验结果,结果发现,当调用user.getGroup().getGroupId()的时候触发了sql,昏倒中,咋办呀,怎么会这样呢?唉,郁闷几分钟后,把hibernate源码复制进工程中开始调试。

一上来就直奔interface LazyInitializer去了,多半就是这里搞的鬼,大家可以先用debug模式跟一下,会发现CGLIB增强的model中有一个LazyInitializer的对象,所以很自然的ctrl+t,看LazyInitializer的实现类有哪几个,基本锁定CGLIBLazyInitializer,考察其中的invoke方法,跟踪到org.hibernate.proxy.pojo.BasicLazyInitializer,其中有如下一方法:
protected final Object invoke(Method method, Object[] args, Object proxy)
基本知道这就是调用group.getGroupId()在运行时调用的代理方法了,找到其中一段话:
else if ( isUninitialized() && method.equals(getIdentifierMethod) ) {
    return getIdentifier();
}

哦,原来是判断,“当前代理对象数据未加载”且“调用的方法就是id的getMethod”,满足此判断的话,返回直接能够直接获取id的Method。也就是说,这就是不触发sql产生的实现代码的所在地,代理对象此时肯定已经有id,没必要再去数据库读。之前说的“诡异现象”,看了这段代码就明白了,只有id的getMethod不为空才能满足这个条件判断,就不会继续执行下去触发sql的产生和执行。

加了打印语句,发现新的实验中getIdentifierMethod为空,导致这个条件判断没有满足,跳过去了。
然后接着继续跟踪为什么getIdentifierMethod是null,先跟到CGLIBLazyInitializer的private构造方法,然后再找到CGLIBProxyFactory的postInstantiate方法,哦getIdentifierMethod是传进来的。

此时有点棘手了,这个类有三个地方引用,定位有点麻烦,花了15分钟,反复调试,定位到org.hibernate.tuple.entity.PojoEntityTuplizer的buildProxyFactory方法,发现此方法的传入参数Getter idGetter为空是导致getIdentifierMethod为空的元凶,打印了一下,发现传入的Getter类形是org.hibernate.property.DirectPropertyAccessor,此时我没有再跟,稍微想了下,基本知道了,我马上把annotation位置换了下,放在getGroupId方法之前:
@Id
@Column(name = "group_id")
public String getGroupId() {
    return groupId;
}

问题解决,后来查了资料确认了情况,原来annotation写在field上,hibenate默认就是认为是访问方式是“field”,如果写在getMethod上,访问方式是“property”。访问方式是“field”的话,就会拿不到之前说的“idGetter”,访问方式是“property”的话就能拿到,这样在调用代理对象的getGroupId()方法的时候getIdentifierMethod不为空,就能通过判断,hibernate发现,哦,你是在调id的getMethod,那就把ID直接给你吧。其实还是hibernate不够智能,应该支持直接通过method的名称,倒推出field的名字,当然这个method必须满足getMethod的特性(返回类型和id的field类型一致,且这个方法没有参数),判断出这个field的名字和id的名字相等,就认为是在调用id的getMehtod就可以了嘛。

新的问题是我又不想把annotation写在getMethod上,咋办呢?改写如下就行了:
@Id
@AccessType(value = "property")//注意这里
@Column(name = "group_id")//实际做的时候没有这一行,用了其他技巧自动转换名字为group_id
private String groupId;

只要ID这么写就行了,别的属性可以不写,官方文档中说不能混用field读取方式和property方式,不用理他的。

至此,问题解决,希望对大家有帮助。

稍微总结一下:
1.传统hbm配置文件和annotation的默认访问不一样,写注释的时候ID要指定用property方式。其实呢,很多照着例子把annotation写在getMethod上是不会碰到我说的这个问题的,但是写在getMethod上总是觉得不自然啊,可读性不佳。
2.只有ID用property方式下,在特定场景下,hibernate才能找到getIdentifierMethod,才能直接返回id,不会触发sql。
3.这个不触发sql的特性本身,大家也应该多多利用,思维方式和传统sql编程方式比较接近:只需要这个外键ID,不需要外键指向的表中的详细数据。

性能问题攸关的相关应用,有兴趣的可以看我的另外一帖:
【基于Java的JSON序列化讨论】
http://www.iteye.com/topic/296467
   发表时间:2008-07-07  
当access=field时,id的getter同样会触发查询语句。
0 请登录后投票
   发表时间:2008-07-07  
hantsy 写道
当access=field时,id的getter同样会触发查询语句。


这是当然,指定方式当然是这样,我的意思是,默认方式下hibernate的行为。
而且hibernate的官方文档中只是说了默认的行为,并没有详细说明,不同的方式会导致什么样的现象,以及这样的现象的详细原因。
0 请登录后投票
   发表时间:2008-07-08  
ctrl+t是干嘛用的? 偶用NB,不知道eclipse下的hot keys
不明白为什么用上了注释了,为什么还要跑出hbm.xml来?在classpath上加一个hibernate-annotations.jar直接用不行吗
0 请登录后投票
   发表时间:2008-07-08  
Joo 写道
ctrl+t是干嘛用的? 偶用NB,不知道eclipse下的hot keys
不明白为什么用上了注释了,为什么还要跑出hbm.xml来?在classpath上加一个hibernate-annotations.jar直接用不行吗


ctrl+t就是直接看一个接口在当前项目下的实现类的层次关系,并且能够直接打开选中的实现类。

此“注释”非彼“注释”,就是在用JDK 1.4的时代,hbm文件产生的方式一般有三种:
1.自己写。
2.自下而上,从数据库中逆向生成hbm文件。
3.自上而下,先写xDoclet注释,形式上比较接近于JDK 1.5中的JPA,然后运行ant脚本,根据xDoclet注释生成hbm文件。

xDoclet注释举例如下,这个是xDoclet2的例子,xDoclet1是要写在getMethod上的:
/**@hibernate.property column="USER_NAME" length="255" type="string" not-null="true"
  */
private String userName;


这就是一般的注释,只不过用@hibernate开头,便于生成hbm的时候识别用。
这个注释和JDK 1.5中的annotation(大家说惯了,也叫注释)不一样。

哦,鉴于此,我还是把原文修改一下,新的注释都叫annotation,这样看起来清楚一点。
0 请登录后投票
   发表时间:2008-07-14  
这个以前真还没有特别滴注意呢
0 请登录后投票
   发表时间:2008-08-06  
没看出来对于程序性能有什么影响。 只是如何使用的问题。
0 请登录后投票
   发表时间:2008-08-28  
xixix2004 写道
没看出来对于程序性能有什么影响。只是如何使用的问题。


原本你是你可以随意的访问外键的id,而不会产生额外的查询sql,比如你用json序列化工具或者是自己写的json序列化方法或者工具,一定会碰到这样的选择题,要不要序列化外键的id。

本来在老版本下这些行为和工具都工作得好好的,升级以后,一不小心,这些工具都产生了额外的sql,产生N+1查询问题,表现出来当然是巨大的性能差别。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics