相对Hibernate等“全自动”ORM机制而言,iBatis以SQL开发的工作量和数据库移植性上的让步,为系统设计提供了更大的自由空间。研究过iBaits以后,发现有些通用的方法可以解决企业级应用的常规工作,就是设计一个通用的持久层对象。
iBatis介绍
使用iBatis提供的ORM机制,对业务逻辑实现人员而言,面对的是纯粹的Java对象,这一层与通过Hibernate实现ORM 而言基本一致,而对于具体的数据操作,Hibernate会自动生成SQL语句,而iBatis则要求开发者编写具体的SQL语句。相对Hibernate等“全自动”ORM机制而言,iBatis以SQL开发的工作量和数据库移植性上的让步,为系统设计提供了更大的自由空间。作为“全自动”ORM 实现的一种有益补充,iBatis的出现显得别具意义。
一、为什么要设计“通用”的东西
在大多数时候,我们所需要的持久层对象(PO)大多都是一张表(or视图)对应一个类。按照Hibernate的思想,就是抛开数据库的束缚,把焦点集中到业务对象中。而很多自动化工具的确做到了通过表结构生成对应的对象,or通过对象自动生成表。对于小项目来说,一切都是简单的;对于有规范设计的项目来说,PO的设计也不是一件困难的工作。但是对于那些业务变动频繁的项目来说,改动PO可能成了一件很繁重的工作。试想一下,假设某个表需要增加一个字段:对于Hibernate(or iBaits),首先要改配置文件,然后PO,然后DAO(也许没有),然后业务逻辑,然后JO,然后界面,etc,贯通了全部层次。
恩,写程序的都不喜欢这些重复劳动,但是做企业级应用的谁不是每天在这些工作中打滚。
研究过iBaits以后,发现有些通用的方法可以解决,就是设计一个通用的持久层对象。
二、基于什么技术
iBatis可以使用Map对象作为PO,Hibernate好像也有相关的功能(我没有细看,不确定)。
iBatis执行一条指令的过程大概是这样的:
|
图1 |
其中圈圈1、2、3描述了iBatis最重要的三个对象。
圈圈1:statement简单来说就是存储sql语句的配置信息,一个最简单的statement:
<statement id=”insertTestProduct” > insert into PRODUCT (PRD_ID, PRD_DESCRIPTION) values (1, “Shih Tzu”) </statement> |
其中id属性是这个statement的唯一标识,全局不能重复。
以上当然是最简单的了,没有参数也不需要返回值,但实际情况下基本都需要传入参数,下面就是介绍参数。
圈圈2:参数对象主要分两种类型:parameterMap、parameterClass和Inline Parameter。
其中parameterMap是配置文件定义传入参数表,如下:
<parameterMap id=”insert-product-param” class=”com.domain.Product”> </parameterMap> <statement id=”insertProduct” parameterMap=”insert-product-param”> insert into PRODUCT (PRD_ID, PRD_DESCRIPTION) values (?,?); </statement> |
而parameterClass是传入参数对象(JavaBean),如下:
<statement id=”statementName” parameterClass=” examples.domain.Product”> insert into PRODUCT values (#id#, #description#, #price#) </statement> |
Inline Parameter则是强化版的parameterClass,如下:
<statement id=”insertProduct” parameterClass=”com.domain.Product”> insert into PRODUCT (PRD_ID, PRD_DESCRIPTION) values (#id:NUMERIC:-999999#, #description:VARCHAR:NO_ENTRY#); </statement> |
其中第一种方法看着就复杂,实际是为了兼容老版本留下来的,所以parameterClass是我们最常用的方法。官方文档对parameterClass介绍很详细,因为这是核心之一,具体请自己查阅。有3个特性说明一下:
a、parameterClass对象可以传入一个Map对象(or Map子类)。本来如果是传入JavaBean,程序会通过get/set来分析取得参数;而Map是key-value结构的,那程序会直接通过key来分析取参数。
b、看以下语句:
<statement id=”statementName” parameterClass=” examples.domain.Product”> insert into PRODUCT values (#id#, #description#, #price#, #classify.id#) </statement> |
蓝色部分#classify.id#翻译过来实际是product.getClassify().getId(),classify是Product对象的一个子对象。
c、在模板sql语句中除了“#”以外,还有“$”,它们两代表的意思当然不同了:
<statement id=”getProduct” resultMap=”get-product-result”> select * from PRODUCT order by $preferredOrder$ </statement> |
“#”在生成sql语句的过程中,会变成“?”,同时在参数表中增加一个参数;
“$”则会直接替换成参数对象对应的值,例如上面的preferredOrder的值可能是“price”,则生成的sql语句就是:
select * from PRODUCT order by price |
*需要特别说明的是传入参数这一部分将会是后面正题“通用持久层对象”的核心,怎么个通用法,怎么设计模板sql语句,都是在这部分上。
圈圈3:结果对象跟参数对象差不多,也有两种,resultMap和resultClass,如下:
resultMap就是配置文件中预定义了要取得的字段:
<resultMap id=”get-product-result” class=”com.iBatis.example.Product”> <result property=”id” column=”PRD_ID”/> <result property=”description” column=”PRD_DESCRIPTION”/> </resultMap> <statement id=”getProduct” resultMap=”get-product-result”> select * from PRODUCT </statement> |
resultClass则是通过分析返回的字段,来填充结果对象:
<statement id="getPerson" parameterClass=”int” resultClass="examples.domain.Person"> SELECT PER_ID as id, PER_FIRST_NAME as firstName FROM PERSON WHERE PER_ID = #value# </statement> |
跟参数对象相反,结果对象一般使用resultMap形式。引用官方的话:使用resultClass的自动映射存在一些限制,无法指定输出字段的数据类型(如果需要的话),无法自动装入相关的数据(复杂属性),并且因为需要ResultSetMetaData的信息,会对性能有轻微的不利影响。但使用resultMap,这些限制都可以很容易解决。
三、正题来了,怎么做“通用持久层对象”
1、表结构
每个表都必须包含两个字段:id和parentId,其他字段按照需求来定义,其他各种索引、约束、关系之类的也按需求定义。
2、通用的持久层对象,CustomPO
public class CustomPO { protected String moduleTable; //该PO对应的表名(视图名) protected int id; //表的id protected int parentID; //父表的id(如果有的话) protected Map fieldMap; //字段Map,核心,用于存储字段及其值 public String getModuleTable() public void setModuleTable(String moduleTable) public int getId() public void setId(int id) public int getParentID() public void setParentID(int parentID) public Map getFieldMap() public void setFieldMap(Map fieldMap) public void copyFieldMap(Map fieldMap) //取得字段名列表 public List getFieldList() //设置字段名列表。如果fieldMap没有相应的字段,则增加,字段值为null;如果有则不增加。 public void setFieldList(List fieldList) //返回字段的“字段名 - 字段值”列表,使用com.fellow.pub.util.KeyValuePair对象作为存储 public List getFieldValueList() } |
那些成员变量的get/set就没什么说的,主要说说getFieldValueList()这个方法。该方法返回一个列表,列表元素是一个key-value结构,简单来说就是把字段map序列化。在构造模板sql语句时会体现它的用途。
3、iBatis对象配置文件CustomPO.xml
<sqlMap namespace="CustomPO"> <!--定义别名--> <typeAlias alias="customPO" type="com.fellow.component.customPO.CustomPO"/> <!-- 通过id查找 特点:iterate这个fieldList列表,生成要输出的字段 --> <select id="customPO_findByID" resultClass="java.util.HashMap" parameterClass="customPO"> SELECT id, parentID <iterate property="fieldList" conjunction=","> $fieldList[]$ </iterate> FROM $moduleTable$ WHERE id = #id# </select>
<!-- 插入一条新纪录 特点:iterate这个fieldValueList列表,分别取得其元素的key和value值 注意$号和#号的使用方法,还有最后怎么取得insert后的id值(各种数据库都可能不同) --> <insert id="customPO_insert" parameterClass="customPO"> INSERT INTO $moduleTable$ (parentID <iterate property="fieldValueList" prepend="," conjunction=","> $fieldValueList[].key$ </iterate> ) VALUES (#parentID# <iterate property="fieldValueList" prepend="," conjunction=","> #fieldValueList[].value# </iterate> ) <selectKey resultClass="int" keyProperty="id"> SELECT last_insert_id() </selectKey> </insert> <!-- 更新一条纪录 特点:iterate这个fieldValueList列表,分别取得其元素的key和value值 注意$号和#号的使用方法 --> <update id="customPO_update" parameterClass="customPO"> UPDATE $moduleTable$ SET <iterate property="fieldValueList" conjunction=","> $fieldValueList[].key$ = #fieldValueList[].value# </iterate> WHERE id = #id# </update> <!--删除一条纪录--> <delete id="customPO_delete" parameterClass="customPO"> DELETE FROM $moduleTable$ WHERE id = #id# </delete>
|
要注意的地方如下:
a、跟一般的iBatis配置文件不一样,该配置中没有包含resultMap,使用的就是resultClass的方式(效率没那么高的那种)。当然,也可以使用resultMap,这样就要为每个表写自己的配置文件了。因此,在该设计没完成前,我暂时先使用resultClass的方式。
b、上面只列举了最简单的增删改以及按id查询,并没有更复杂的查询,为什么呢?因为我还在研究中。。。研究通用的模板sql的写法。
4、CustomPO对应的DAO
我使用了ibaits提供的DAO框架,很好用,不单支持iBatis的框架,还支持Hibernate、JDBC等等,而且是与iBatis本身独立的,完全可以单独使用。以后就不用自己写DAO框架了。该DAO接口是:
public interface ICustomDAO { /** * 通过传入moduleTable和id取得一条记录 */ CustomPO findByID(String moduleTable, int id) throws Exception; /** * 通过传入CustomPO对象取得一条记录 * @param po CustomPO 该对象在传入前应该先设置moduleTable和id参数, * 并且使用setFieldList()函数设置字段列表(该设置决定所返回的字段)。 */ CustomPO findByID(CustomPO po) throws Exception; /** * 通过传入moduleTable和parentID取得一条记录 */ CustomPO findByParentID(String moduleTable, int parentID) throws Exception; /** * 通过传入CustomPO对象插入一条记录 * @param po CustomPO 该对象在传入前应该先设置moduleTable和id参数, * 并且使用setFieldMap()函数设置“字段-值”对。 */ void insert(CustomPO po) throws Exception; /** * 通过传入CustomPO对象更新一条记录 * @param po CustomPO 该对象在传入前应该先设置moduleTable和id参数, * 并且使用setFieldMap()函数设置“字段-值”对。 */ void update(CustomPO po) throws Exception; /** * 删除moduleTable和id所对应的记录 */ void delete(String moduleTable, int id) throws Exception; } |
我没有把所有的方法都列出来,反正挺简单的,跟一般的DAO没什么分别。
另外列几个实现的片断:
a、统一的数据装填函数,需要手工把id和parentID字段去掉。
protected void fill(Map result, CustomPO po) { Long returnId = (Long) result.get("id"); Long returnParentID = (Long) result.get("parentID"); result.remove("id"); result.remove("parentID"); if (returnId != null) po.setId(returnId.intValue()); if (returnParentID != null) po.setParentID(returnParentID.intValue()); po.setFieldMap(result); } |
b、一般的查询,返回的是一个map,然后再用fill()函数。
//查询 Map result = (Map)this.queryForObject("customPO_findByID", po); //处理返回结果 if(result == null) po = null; else fill(result, po); |
c、增删改,没有返回值,值得一提的是增加操作完成后,po里面的id会更新,具体看前面相关的statement。
//增删改 this.insert("customPO_insert", po); this.update("customPO_update", po); this.delete("customPO_delete", po); |
5、前面是通用的部分,光是通用是不够的。因此我另外建立了一套配置文件,记录字段对应关系。看看我所定义的一个配置文件,挺简单的:
<struct name="userInfo" table-name="tblUserInfo"> <field name="昵称" column="NICK_NAME" type="string" not-null="true" unique="false" /> <field name="姓名" column="FULL_NAME" type="string" not-null="true" unique="false" /> <field name="性别" column="SEX" type="string" not-null="false" unique="false" /> </struct> |
其中,name是字段名,column是字段对应数据表的字段名,type是字段类型,not-null是是否不能为空,unique是是否唯一。只有name这个属性是必填的,column如果不填默认与name相等,type默认为string,not-null和unique默认为false。
配置文件的读取类在这里就省略了。
为什么要自己定义一套这个框架?好像挺多此一举的,但是没办法,iBatis配置文件的信息是封闭的,我无法取得。另外我考虑的是:
a、viewer层
在web开发中,我可以在这套配置框架的基础上,建立自己的标签,实现数据自动绑定的功能;GUI开发中也可以做相应的功能。
b、module层
可以做通用的业务操作,不需要为每个业务都都做独立的业务逻辑。
四、有什么优点、缺陷
1、优点
a、“通用”,一切都是为了通用,为了减少重复劳动,一个个项目面对不同的业务,其实说到底都是那些操作。各种持久成框架已经带给我们不少的方便,但是在实际业务逻辑的处理上好像还没有一个这样的框架。
b、极大地减少代码量。前面说了,数据库改一个字段,PO、DAO、module、JO、viewer、validator全都要改一遍。使用了这套东西,可以把绝大部分的劳动都放在配置文件和UI上。当然,这是完美的设想,对于很多情况,业务逻辑还是要手工写的。
c、好像没有c了。
2、缺点
a、通常通用的东西都缺乏灵活性,在我的实际应用中也发现了这样那样的问题,解决方法都是以通用为基本原则。但是如果针对的是某个项目,那就可以针对业务来修改了。
b、性能问题。因为使用resultClass,按照文档所说的,性能没有resultMap好。当然也可以使用resultMap,如前所说,就要对每个PO写配置文件了,工作量也不少。
c、也好像没有c了。
五、后话
我总是喜欢写一些通用的东西,总是想把它设计成万能的。但是经过多次失败总结出来,我发现通用的东西在很多情况下都等于不能用。但我就是喜欢往通用方面想,这个毛病不知道什么时候才能改得了。其实在Delphi平台中,我早就实现了相关的东西,但是用delphi总是限制于ADO+Data Module这样掉牙的模式。现在转向java,发现有iBatis、hibernate这么多好的持久层框架,自然有移植必要了。
查看本文来源