Hibernate的POJO到JSON序列化过程的讨论

一年前的代码,最近写了点文档share大家,有点乱,这个乱主要指夹杂了些我们公司开发框架的一些东西。
如果看到错误或者莫名其妙的地方还请指出,包括错别字。
此文纯属个人观点,欢迎讨论。

论坛关联的地址:
[url]http://www.iteye.com/topic/296467[/url]

我认为的“智能”的JSON序列化方案主要包含如下特点:
1.要像JSON-lib那样能够利用反射和递归,自动遍历对象图,同时并序列化,这个要求JSON-lib已经满足。
2.默认情况下,所有的简单对象(非Hibernate代理对象)都能够自动序列化,利用JSON-lib就能实现。
3.所有的Hibernate代理对象自动屏蔽。
4.提供足够灵活和足够简单的参数方式,有选择的序列化指定的Hibernate代理对象。
5.对Hibernate多对一的代理对象提供足够的支持,能够直接抓取外键ID而不触发多余的sql,这个多对一的性能问题可以参考自我的另外一帖的分析:
【Hibernate 3.2中annotation注释的位置对性能的巨大影响】
[url]http://www.iteye.com/topic/212236[/url]

有兴趣的人参看附件中的源代码。

先介绍JSON的基本概念,至于序列化成JSON的目的后问会详细叙述。JSON:JavaScript Object Notation,是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScript(Standard ECMA-262 3rd Edition - December 1999)的一个子集。
来看一个NodeUser的部分属性的最简单的例子:

{"nusId":"402892891b209674011b209681530009","nusCreateTime":"2008-05-06 20:21:34"}

可以理解为一个map结构的数据结构,一个key(属性名),一个value(属性值),只不过都是文本类型。
看一个数组或者Collection转换成JSON后的例子:

[
{"nusEmail":"john@gmail.com","nusStatus":{"code":"1040-1010","name":"身份证"}},
{"nusEmail":"david@hotmail.com","nusStatus":{"code":"1040-1020","name":"军人证"}}
]

就是用中括号来表示多个对象的集合。在这例子中还出现了对象的嵌套,nusStatus是一个枚举对象,之前已经介绍过,只有code和name两个属性,从数据库读取数据以后,只有code有值,name的值一开始是空的,随意JSON序列化的过程除了把Java对象转换成JSON文本以外,还要把所有的枚举类中的name的值填上,最后才把序列化好的JSON字符串传递到客户端浏览器,JSON通讯策略后文会分析。

分析一个性能问题:JSON序列化和Hibernate放在一起使用其实就是一个数据抓取的过程,因为JSON序列化的时候遍历的的是Hibernate生成的实体类的每一个属性,表面上看访问的是属性,但是实际上是Hibernate生成的代理对象,调用属性的get方法的过程某种意义上来说就是一个数据抓取的过程。Hibernate的性能问题绝大部分都是出现在数据抓取的粒度和深度问题。所以JSON-lib的工具类net.sf.json.JSONObject和net.sf.json.JSONArray是不能满足本框架的需求的,或者说是不能直接和Hibernate结合使用的。
一般大多数的JSON序列化的做法是在每一个实体类中实现一个toJSON()的方法来直接提供序列化的支持,我认为这种做法是非常的有问题的,实体类的结构是要充分利用的,也就意味着一个实体类的数据抓取粒度和深度在不同场景下是完全不一样的,不可能用一个toJSON()方法就能完全搞定。再有就是JSON序列化属于和Ajax客户端通讯是才使用的机制,把Ajax通讯相关的代码写在实体类中,从软件分层角度上来说也是非常不合适的。

举一个抓取粒度和深度不同的例子:先参考前文描述的NodeEducation和NodeUser的多对一的关系。第一个场景是,需要列出所有教育背景的基本信息。第二个场景是,列出所有教育背景的基本信息,外加所属用户的姓名,这个需求下,数据抓取一定会牵涉到NodeUser所对应的表,这两个场景的数据抓取策略肯定是不一样的。

介绍JSONConvert类中的三个方法:
modelCollect2JSONArray方法,把Collection转换为JSONArray对象,实现方式就是遍历需要转换的Collection,针对每一个model再次调用另一个专门转换model方法,最后返回JSONArray对象。

model2JSON是最重要的方法,把model转换为JSON对象,凡是实现JSONNotAware的不序列化,按照get方法得到属性信息,默认对所有可能引起的Hibernate查询数据的sql的操作都会跳过,目的是减少不必要的延迟加载,如果一定要要抓取的话,则把需要序列化的名字通过jsonAwareArray传入。

getAwareSubList方法是用来处理每一级的递归需要的字符串处理,从原始的jsonAwareList中查找前缀为name的,去除前缀后,放到新的List中返回。

结合一个具体的例子详细分析如下,先不考虑分页:

场景一:需要列出所有教育背景的基本信息。

NodeEducationService中代码如下,因为利用的BaseManager,所以NodeEducationManager针对这个查询中没有特别的代码:

public List findList() {
return nodeEducationManager.findList();
}

Ctrl类最终会间接的调用modelCollect2JSONArray(nodeEducationList),Ctrl的代码规范和运行过程后文会详细介绍,这里的list就是List,modelCollect2JSONArray遍历每一个元素,调用model2JSON(nodeEducation),这个方法会根据反射的结果遍历所有的nodeEducation的所有method。为什么是遍历方法,而不是遍历属性是有重要原因的,例如一个文件类,只保存了完整的文件名的这一个属性,但是提供了很多get方法,有获得文件名前缀、后缀的等等,虽然页面上编程也能达到同样的效果,但是实体类中既然已经提供了这样便利的方法,就应该充分利用,就应该根据get方法造两个属性“文件名前缀”、“文件名后缀”出来,这样能充分减少页面上的编程工作量。

继续分析序列化单实例的过程,在遍历所有的方法时,会判断当前方法是否符合getMethod的特征,符合的才序列化这个getMethod对应的虚拟属性,因为这个属性未必真实存在。此时还会过滤一些明显不需要序列化的get方法,例如getClass方法、或者是getMethod返回类型是Document.class(XML Document对象)、byte[].class(二进制文件对象)、Logger.class(系统日志对象)、LazyInitializer.class(Hibernate代理对象中的延迟加载属性),这些都是明显不需要序列化的对象。

然后继续判断,如果getMethod返回格式是Calendar和Date格式,则调用相应的转换方法,转换成统一的字符串格式,如果是枚举类格式,则调用dictionaryFactory中的setName方法,根据枚举类的code找到对应的name,并赋值到给这个枚举类的name。、

如果getMethod返回格式是JSONNotAware,说明遇到了多对一的属性,如果继续访问这个这个返回对象的其他值就有可能要触发sql,所以跳过。

如果getMethod返回格式是org.hibernate.collection.AbstractPersistentCollection,说明遇到了一对多的属性,如果继续访问这个这个返回对象的其他值就有可能要触发sql,必须跳过。

对于场景一序列化大致就是这个过程,但是对于多对一的跳过的说法其实是不完全的,先分析一下,假设页面上有这样的需求,每一个教育背景信息占一行,每一行有一个链接“查看所属用户”,指向一个新页面,那这个url里的get参数肯定是要带上所属用户id的,到了显示用户的页面再根据次id查询相关用户信息,也就是说序列化的内容中需要多对一属性nodeUser的主键nusId,在表node_education中就是普通的外键,这项信息本来就是应该存在的,不需要再次触发什么sql来查询node_user表。而且Hibernate也确实提供了这样的机制来获取这个外键id,而不会触发新的查询sql,一旦触发就是著名的N+1查询问题,有10个教育背景可能就产生11条sql语句。

现在详细说明获取这个外键id的过程,首先判断这个返回对象是不是HibernateProxy的类型,不是的话说明不是Hibernate多对一的代理对象。然后利用ModelUtils.getIdFieldName((Modelable) getObj)获取idName,利用(((HibernateProxy) getObj).getHibernateLazyInitializer()).getIdentifier()获取id的值,构造一个只有id的JSONObject对象进行序列化。也就是说,序列化的时候,默认总是会序列化多对一的那个对象的主键,也就是主表的外键,因为这些数据应经抓取到了,不会触发额外的sql影响性能。最后得到的序列化的结果示例如下:

{
"nedId":"4028928d1c744a03011c744a03770000",
"nedCreateTime":"2008-10-06 20:12:21",
"nodeUser":{"nusId":"4028928d1c744a03011c744a03770000"}
}

省略了部分属性,注意这里的nodeUser嵌套对象只有nusId这一个属性。

场景二,如果需求变化,在这个教育背景列表中的每一行都要显示所属用户的姓名,从数据库的角度来分析,这是一定要查询第二张表才能获取到数据的。NodeUserManager中新增一个方法:

public List findListWithNodeUser() {
String hql = "from NodeEducation ned join ned.nodeUser";
return getQuery(hql).list();
}

虽然这是一个跨表的查询,但是不是一个跨表的业务操作,所以可以放在Manager中。NodeUserService也增加一个同名方法:

public List findListWithNodeUser() {
return nodeEducationManager.findListWithNodeUser();
}

之后和场景一类似,不同之处在于Ctrl类最终会的调用代码类似下面的例子:

List jsonAwareCollect = new LinkedList();
jsonAwareCollect.add("nodeUser");
modelCollect2JSONArray(nodeEducationList, jsonAwareCollect);

jsonAwareCollect是第二个参数,其中指定的属性名字会强制序列化,在判断多对一和一对多的场景前,会先判断jsonAwareCollect参数中是否含有当前属性的名称,有的话会强制序列化,所以最终的序列化结果示例如下:

{
"nedId":"4028928d1c744a03011c744a03770000",
"nedCreateTime":"2008-10-06 20:12:21",
"nodeUser":{"nusId":"4028928d1c744a03011c744a03770000","nusEmail":"john@live.com"}
}

序列化过程会经由一个递归过程完整遍历nodeUser对象,getAwareSubList方法就是每次递归之前处理下一次递归所需的jsonAwareCollect用的,例如,如果jsonAwareCollect的代码如下,假设nodeUser中还有个多对一对象group:

jsonAwareCollect.add("nodeUser");
jsonAwareCollect.add("nodeUser.group");

表示继续序列化实体类group,实际上上面的例子只需第二行就行了,因为第二行肯定会序列化nodeUser对象了,第一行就不需要了。至此说明了jsonAwareCollect就是可定制序列化的核心,通过简单明了的参数传递就能够很方便的进行序列化的定制,达到既能只获取必须数据,又不会过多的抓取数据的效果。在场景二的例子中,因为Manager中的实现已经做了两表的关联查询,相关的nodeUser数据早已抓取完毕,所以序列化nodeUser的时候不会触发新的sql,也就是说通过配套的查询方法,能够和JSON序列化工具很好的配合,最终只产生一句sql,就能抓取并序列化所有需要数据。

但是这种配合在一对多下是不成立,本质上因为,一对多很难用一句sql搞定所有数据,此时要考虑UI交互问题,而不是强行的一次性抓取数据。例,场景三,罗列当前数据库中所有NodeUser用户的基本信息,同时还要显示每个用户的所有教育背景资料,初看是一个必须要抓取每个nodeUser的一对多属性nodeEducationList的例子,但是实际上这个需求本身就不合理,这么多信息在一个页面本来就难以显示,实际一点的做法往往是,给一个名为“查看所有教育背景”的链接,点击以后,传递一个所在行nodeUser的id到新的页面,服务端重新根据传递的id来查询次用户下的所有教育背景资料,第二次查询用Hibernate来做也是很简单的,花不了多少工作量,但是逻辑非常清楚。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值