suixinsuoyu 2013-06-12
http://www.ibm.com/developerworks/cn/java/j-lo-ibatis-principle/
许令波([email protected]),Java工程师,淘宝网
简介:iBATIS通过SQLMap将Java对象映射成SQL语句和将结果集再转化成Java对象,与其他ORM框架相比,既解决了Java对象与输入参数和结果集的映射,又能够让用户方便的手写使用SQL语句。本文主要介绍了iBATIS框架的体系结构和运行流程,以及iBATIS如何完成SQL语句的解析与Java对象与数据字段映射关系的建立,最后用一个实例说明了iBATIS是如何帮我们完成工作的。
发布日期:2010年11月08日
级别:高级
访问情况:27963次浏览
评论:5(查看|添加评论-登录)
平均分(93个评分)
为本文评分
iBATIS框架主要的类层次结构
总体来说iBATIS的系统结构还是比较简单的,它主要完成两件事情:
根据JDBC规范建立与数据库的连接;
通过反射打通Java对象与数据库参数交互之间相互转化关系。
iBATIS的框架结构也是按照这种思想来组织类层次结构的,其实它是一种典型的交互式框架。先期准备好交互的必要条件,然后构建一个交互的环境,交互环境中还划分成会话,每次的会话也有一个环境。当这些环境都准备好了以后,剩下的就是交换数据了。其实涉及到网络通信,一般都会是类似的处理方式。
图1是iBATIS框架的主要的类层次结构图:
图1.iBATIS框架的主要的类层次结构图
上面的类图中左边SqlMapClient接口主要定义了客户端的操作行为包括select、insert、update、delete。而右边主要是定义了当前客户端在当前线程的执行环境。SqlMapSession可以共享使用,也可以自己创建,如果是自己创建在结束时必须要调用关闭接口关闭。
当使用者持有了SqlMapClientImpl对象就可以使用iBATIS来工作了。这里还要提到另外一个类SqlMapExecutorDelegate这个类从名字就可以看出他是执行代理类。这个类非常重要,重要是因为他耦合了用户端的执行操作行为和执行的环境,他持有执行操作的所需要的数据,同时提供管理着执行操作依赖的环境。所以他是一个强耦合的类,也可以看做是个工具类。
回页首
iBATIS框架的设计策略
iBATIS主要的设计目的还是为了让我们执行SQL时对输入输出的数据管理更加方便,所以如何方便的让我们写出SQL和方便的获取SQL的执行结果才是iBATIS的核心竞争力。那么iBATIS是怎么实现它的核心竞争力的呢?
iBATIS框架的一个重要组成部分就是其SqlMap配置文件,SqlMap配置文件的核心是Statement语句包括CIUD。iBATIS通过解析SqlMap配置文件得到所有的Statement执行语句,同时会形成ParameterMap、ResultMap两个对象用于处理参数和经过解析后交给数据库处理的Sql对象。这样除去数据库的连接,一条SQL的执行条件已经具备了。
图2描述了Statement有关的类结构图:
图2.Statement有关的类结构图
图2给出了围绕SQL执行的基本的结构关系,但是还有一个关键的部分就是,如何定义SQL语句中的参数与Java对象之间的关系,这其中还涉及到Java类型到数据库类型的转换等一系列问题。
数据的映射大体的过程是这样的:根据Statement中定义的SQL语句,解析出其中的参数,按照其出现的顺序保存在Map集合中,并按照Statement中定义的ParameterMap对象类型解析出参数的Java数据类型。并根据其数据类型构建TypeHandler对象,参数值的复制是通过DataExchange对象完成的。
图3是参数映射相关的类结构图:
图3.参数映射相关的类结构图
图3是输入参数的映射结构情况,返回结果ResultMap的映射情况也是类似的。主要就是要解决SQL语句中的参数与返回结果的列名与Statement中定义的parameterClass和resultClass中属性的对应关系。
回页首
iBATIS框架的运行原理
前面大体分析了iBATIS框架的主要类的结构,这里主要看一下这些类是如何串联起来、如何工作的。图4描述了整个过程的主要执行步骤。
图4.iBATIS运行的主要执行步骤
上图中描述的SqlMapSession对象的创建和释放根据不同情况会有不同,因为SqlMapSession负责创建数据库的连接,包括对事务的管理,iBATIS对管理事务既可以自己管理也可以由外部管理,iBATIS自己管理是通过共享SqlMapSession对象实现的,多个Statement的执行时共享一个SqlMapSession实例,而且都是线程安全的。如果是外部程序管理就要自己控制SqlMapSession对象的生命周期。
图5是通过Spring调用iBATIS执行一个Statement的一个详细的时序图:
图5.Spring调用iBATIS执行一个Statement的时序图
(查看图5的清晰版本。)
iBATIS的主要工作连接、交互,所以必须根据不同的交易成本设计不同的交易环境。
回页首
示例
下面我们将根据一个具体的实例解析一个Statement如何完成映射的,我们用一个典型的查询语句看看Java对象中的数据时如何赋给SQL中的参数的,再看看SQL的查询结果是如何转成Java对象的。
先看一下示例的部分代码和配置文件,完整的代码请看附件。
Spring的applicationContext配置文件:
清单1.applicationContext.xml
<beans>
<beanid="sqlMapTransactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<propertyname="dataSource"ref="dataSource"/>
</bean>
<beanid="sqlMapTransactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<propertyname="transactionManager"ref="sqlMapTransactionManager"/>
</bean>
<!--sqlmap-->
<beanid="sqlMapClient"
class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<propertyname="configLocation"value="com/mydomain/data/SqlMapConfig.xml"/>
<propertyname="dataSource"ref="dataSource"/>
</bean>
<beanid="dataSource"name="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close">
<propertyname="driverClassName"value="oracle.jdbc.driver.OracleDriver"/>
<propertyname="url"value="jdbc:oracle:thin:@10.1.5.11:1521:XE"/>
<propertyname="username"value="junshan"/>
<propertyname="password"value="junshan"/>
<propertyname="maxActive"value="20"/>
</bean>
<beanid="accountDAO"class="com.mydomain.AccountDAO">
<propertyname="sqlMapClient"ref="sqlMapClient"/>
<propertyname="sqlMapTransactionTemplate"ref="sqlMapTransactionTemplate"/>
</bean>
</beans>
下面是Account.xml的一个Statement:
清单2.Account.xml中一个Statement
<selectid="selectAccount"parameterclass="Account"resultclass="Account">
select
ACC_ID,
ACC_FIRST_NAMEasfirstName,
ACC_LAST_NAMEaslastName,
ACC_EMAILasemailAddress,
ACC_DATE
fromACCOUNT
whereACC_ID=#id:INTEGER#andACC_FIRST_NAME=#firstName#
</select>
下面是Java的测试类:
清单3.SimpleTest
publicclassSimpleTest{
publicstaticvoidmain(String[]args){
ApplicationContextfactory=
newClassPathXmlApplicationContext("/com/mydomain/data/applicationContext.xml");
finalAccountDAOaccountDAO=(AccountDAO)factory.getBean("accountDAO");
finalAccountaccount=newAccount();
account.setId(1);
account.setFirstName("tao");
account.setLastName("bao");
account.setEmailAddress("[email protected]");
account.setDate(newDate());
try{
accountDAO.getSqlMapTransactionTemplate().execute(newTransactionCallback(){
publicObjectdoInTransaction(TransactionStatusstatus){
try{
accountDAO.deleteAccount(account.getId());
accountDAO.insertAccount(account);
//account.setLastName("bobo");
//accountDAO.updateAccount(account);
Accountresult=accountDAO.selectAccount(account);
System.out.println(result);
returnnull;
}catch(Exceptione){
status.setRollbackOnly();
returnfalse;
}
}
});
//accountDAO.getSqlMapClient().commitTransaction();
}catch(Exceptione){
e.printStackTrace();
}
}
}
回页首
iBATIS对SQL语句的解析
这里所说的SQL解析只是针对iBATIS配置文件中所定义的SQL语句,如前一节中清单2中所示的查询语句。和标准的SQL语句不同的是,参数的赋值是“#“包裹的变量名。如何解析这个变量就是iBATIS要完成的工作。当然SQL的表达形式还有很多其他的形式如动态SQL等。
现在我们关心的是当我们执行:
清单4.执行查询方法
accountDAO.selectAccountById(account)
iBATIS将会选择清单2这条Statement来解析,最终会把它解析成一个标准的SQL提交给数据库执行,并且会设置两个选择条件参数。这个过程中参数映射的细节是什么样子呢?
在前面的第二小节中已经说明了,iBATIS会把SqlMap配置文件解析成一个个Statement,其中包括ParameterMap、ResultMap,以及解析后的SQL。当iBATIS构建好RequestScope执行环境后,要做的工作就是把传过来的对象数据结合ParameterMap中信息提取出一个参数数组,这个数组的顺序就是对应于SQL中参数的顺序,然后会调用preparedStatement.setXXX(i,parameter)提交参数。
在清单3中,我们给account对象的id属性和firstName属性分别赋值为1和“tao“,当执行清单4中的这段代码时,iBATIS必须把这两个属性值传给清单2中SQL语句中对象的参数。这个是怎么做到的,其实很简单,在图3中描述了与ParameterMap相关的类的关系,这些类中都保存了在SqlMap配置文件初始化是解析清单2中Statement的所有必要的信息,具体的信息是这样的:
最终的SQL语句是:
清单5.解析后的SQL
select
ACC_ID,
ACC_FIRST_NAMEasfirstName,
ACC_LAST_NAMEaslastName,
ACC_EMAILasemailAddress,
ACC_DATE
fromACCOUNT
whereACC_ID=?andACC_FIRST_NAME=?
#id:INTEGER#将被解析成JDBC类型是INTEGER,参数值取Account对象的id属性。#firstName#同样被解析成Account对象的firstName属性,而parameterclass="Account"指明了Account的类类型。注意到清单5中#id:INTEGER#和#firstName#都被替换成“?”,iBATIS如何保证它们的顺序?在解析清单2过程中,iBATIS会根据“#”分隔符取出合法的变量名构建参数对象数组,数组的顺序就是SQL中变量出现的顺序。接着iBATIS会根据这些变量和parameterClass指定的类型创建合适的dataExchange和parameterPlan对象。parameterPlan对象中按照前面的顺序保存了变量的setter和getter方法列表。
所以parameter的赋值就是根据parameterPlan中保存的getter方法列表以及传进来的account对象利用反射机制得到清单5对应的参数值数组,再将这个数组按照指定的JDBC类型提交给数据库。以上这些过程可以用图6的时序图清楚的描述:
图6.映射参数值到数据库过程时序图
上图4中在8步骤中如果value值为空时会设置preparedStatement.setNull(i,jdbcType)如果在清单2中的变量没有设置jdbcType类型时有可能会出错。
回页首
数据库字段映射到Java对象
数据库执行完SQL后会返回执行结果,在第4小节的例子中满足id为1、firstName为“tao”的信息有两条,iBATIS如何将这两条记录设置到account对象中呢?
和ParameterMap类似,填充返回信息需要的资源都已经包含在ResultMap中。当有了保存返回结果的ResultSet对象后,就是要把列名映射到account对象的对应属性中。这个过程大体如下:
根据ResultMap中定义的ResultClass创建返回对象,这里就是account对象。获取这个对象的所有可写的也就是setter方法的属性数组,接着根据返回ResultSet中的列名去匹配前面的属性数组,把匹配结果构造成一个集合(resultMappingList),后面是选择DataExchange类型、AccessPlan类型为后面的真正的数据交换提供支持。根据resultMappingList集合从ResultSet中取出列对应的值,构成值数组(columnValues),这个数组的顺序就是SQL中对应列名的顺序。最后把columnValues值调用account对象的属性的setter方法设置到对象中。这个过程可以用下面的时序图来表示:
图7.映射返回对象时序图
回页首
示例运行的结果
前两个小节主要描述了输入参数和输出结果的映射原理,这里再结合第4小节的示例分析一下执行清单3代码的结果。
执行清单3所示代码打印的结果为:
清单6.示例程序的运行结果
Account{id=0,firstName='tao',lastName='bobo',emailAddress='[email protected]'}
上面的结果和我们预想的结果似乎有所不同,看代码我们插入数据库的account对象各属性值分别为{1,“tao”,“bao”,“[email protected]”,“时间”},后面调用清单2的查询,返回应该是一样的结果才对。id的结果不对、date属性值丢失。再仔细看看清单2这个Statement可以发现,返回结果的列名分别是{ACC_ID,firstName,lastName,emailAddress,ACC_DATE}其中id和date并不能映射到Account类的属性中。id被赋了默认数字0,而date没有被赋值。
还有一个值得注意的地方是变量id后面跟上JDBC类型,这个JDBC类型有没有用?通常情况下都没有用,因此你可以不设,iBATIS会自动选择默认的类型。但是如果你要这个这个值可能为空时如果没有指定JDBC类型可能就有问题了,在Oracle中虽然能正常工作但是会引起Oracle这当前这个SQL有多次编译现象,因此会影响数据库的性能。还有当同一个Java类型如果对应多个JDBC类型(如Date对应JDBC类型有java.sql.Date、java.sql.Timestamp)就可以通过指定JDBC类型保存不同的值到数据库。
回页首
总结
如果用最简洁的话来总结iBATIS主要完成那些功能时,我想下面几个代码足够概括。
清单7.典型的Java操作数据库代码
Class.forName("oracle.jdbc.driver.OracleDriver");
Connectionconn=DriverManager.getConnection(url,user,password);
java.sql.PreparedStatementst=conn.prepareStatement(sql);
st.setInt(0,1);
st.execute();
java.sql.ResultSetrs=st.getResultSet();
while(rs.next()){
Stringresult=rs.getString(colname);
}
iBATIS就是将上面这几行代码分解包装,但是最终执行的仍然是这几行代码。前两行是对数据库的数据源的管理包括事务管理,3、4两行iBATIS通过配置文件来管理SQL以及输入参数的映射,6、7、8行是iBATIS获取返回结果到Java对象的映射,他也是通过配置文件管理。
配置文件对应到相应代码如图所示:
图8.配置文件与相应代码对应关系
iBATIS要达到目的就是把用户关心的和容易变化的数据放到配置文件中配置,方便用户管理。而把流程性的、固定不变的交给iBATIS来实现。这样是用户操作数据库简单、方便,这也是iBATIS的价值所在。
回页首
下载
描述名字大小下载方法
本文示例代码code_example.rar7KBHTTP
关于下载方法的信息
参考资料
学习
“扩展iBatis以透明支持多种数据库”(developerWorks,2007年12月):本文提供了一个简单有效的方法,通过扩展iBatis来透明地支持多数据库方言。
“iBATIS3内的新特性”(developerWorks,2010年4月):本文将介绍iBATIS3内的一些新特性。
如:对于sql语句order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id"。