原贴:
[url]http://www.iteye.com/topic/212236[/url]
近日把自己的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"/>
在这个配置的默认情况下,分析如下代码:
首先第二行不会产生select ... from group WHERE group_id=? 的代码。
关键的是第三行也不会产生,直到第四行才会触发这条sql。
这里还有奇怪的现象,如果在Group.java中有如下代码:
然后同等条件执行user.getGroupIdTest01()和user.getGroupIdTest02()都会触发sql语句的生成,昏倒啊,暂且作伏笔,先不讨论这个诡异现象。
先介绍这个不触发sql的用途和应用场景。
比如在一个页面上要显示用户列表(不管是写页面方式,还是JSON序列化都一样),最后一列有一个按钮,按钮上的文字是“显示所属小组详细信息”,那这个按钮触发的事件可能是个弹出窗口或者URL跳转,一般要带个参数groupId=xxx来定位要显示的小组ID,此时数据抓取深度显然不需要group的其他信息,如果触发上面提到的sql,势必产生N+1的性能问题(这也是标题中“巨大”的由来),其实如果事先知道要小组的其它信息,这个页面显示的做法就完全是其他策略了。也就是说,确保调用user.getGroupId()不会产生新的sql是很重要的,能减少很多麻烦。
然后说说JDK 1.5下,当我写新的例子:
我为了确保原先的策略不会有问题,特地又做了个实验结果,结果发现,当调用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()在运行时调用的代理方法了,找到其中一段话:
哦,[color=red][b]原来是判断,“当前代理对象数据未加载”且“调用的方法就是id的getMethod”,满足此判断的话,返回直接能够直接获取id的Method。[/b][/color]也就是说,这就是不触发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方法之前:
问题解决,后来查了资料确认了情况,原来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这么写就行了,别的属性可以不写,官方文档中说不能混用field读取方式和property方式,不用理他的。
至此,问题解决,希望对大家有帮助。
稍微总结一下:
1.传统hbm配置文件和annotation的默认访问不一样,写注释的时候ID要指定用property方式。
2.只有ID用property方式下,在特定场景下,hibernate才能找到getIdentifierMethod,才能直接返回id,不会触发sql。
3.这个不触发sql的特性本身,大家也应该多多利用,思维方式和传统sql编程方式比较接近:只需要这个外键ID,不需要其他表中的详细数据。
[url]http://www.iteye.com/topic/212236[/url]
近日把自己的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的其他信息,如果触发上面提到的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();
}
哦,[color=red][b]原来是判断,“当前代理对象数据未加载”且“调用的方法就是id的getMethod”,满足此判断的话,返回直接能够直接获取id的Method。[/b][/color]也就是说,这就是不触发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方式。
2.只有ID用property方式下,在特定场景下,hibernate才能找到getIdentifierMethod,才能直接返回id,不会触发sql。
3.这个不触发sql的特性本身,大家也应该多多利用,思维方式和传统sql编程方式比较接近:只需要这个外键ID,不需要其他表中的详细数据。