小小的声明:该文章已优先发在阿里内网博客
ibatis已成过去式,官方早已不再维护,使用ibatis的老应用迁移到mybatis很有必要,且好用的服务层框架springboot集成了mybatis,支持维护良好,更加说明了迁移到mybatis的重要性,下面详细说明整个迁移流程
转换工具:ibatis2mybatis
可以帮你将ibatis 2.x sqlmap文件转换为myBatis 3.x mapper文件,该工具是使用了Ant构建任务进行XSTL转换和一些语法文字替换
该工具下载下来使用非常简单,把你要转换的所有sqlmap文件放到source文件夹,然后在当前目录直接运行ant命令即可,转换成功的mapper文件放在了destination文件夹下。不过遗憾的是,ant命令并不总是运行成功,跟你的sqlmap文件有关,比如我当时转换时报错如下:
BUILD FAILED
/Users/zhumin/Documents/ibatis2mybatis-ibatis2mybatis-1.0/build.xml:24: javax.xml.transform.TransformerException: javax.xml.transform.TransformerException: com.sun.org.apache.xml.internal.utils.WrappedRuntimeException: unknown protocol: classpath
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:749)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:351)
at org.apache.tools.ant.taskdefs.optional.TraXLiaison.transform(TraXLiaison.java:197)
at org.apache.tools.ant.taskdefs.XSLTProcess.process(XSLTProcess.java:844)
at org.apache.tools.ant.taskdefs.XSLTProcess.execute(XSLTProcess.java:434)
at org.apache.tools.ant.UnknownElement.execute(UnknownElement.java:293)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.tools.ant.dispatch.DispatchUtils.execute(DispatchUtils.java:106)
at org.apache.tools.ant.Task.perform(Task.java:348)
at org.apache.tools.ant.Target.execute(Target.java:435)
at org.apache.tools.ant.Target.performTasks(Target.java:456)
at org.apache.tools.ant.Project.executeSortedTargets(Project.java:1405)
at org.apache.tools.ant.Project.executeTarget(Project.java:1376)
at org.apache.tools.ant.helper.DefaultExecutor.executeTargets(DefaultExecutor.java:41)
at org.apache.tools.ant.Project.executeTargets(Project.java:1260)
at org.apache.tools.ant.Main.runBuild(Main.java:854)
at org.apache.tools.ant.Main.startAnt(Main.java:236)
at org.apache.tools.ant.launch.Launcher.run(Launcher.java:285)
at org.apache.tools.ant.launch.Launcher.main(Launcher.java:112)
Caused by: javax.xml.transform.TransformerException: com.sun.org.apache.xml.internal.utils.WrappedRuntimeException: unknown protocol: classpath
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.getDOM(TransformerImpl.java:578)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:739)
... 21 more
Caused by: com.sun.org.apache.xml.internal.utils.WrappedRuntimeException: unknown protocol: classpath
at com.sun.org.apache.xalan.internal.xsltc.dom.XSLTCDTMManager.getDTM(XSLTCDTMManager.java:427)
at com.sun.org.apache.xalan.internal.xsltc.dom.XSLTCDTMManager.getDTM(XSLTCDTMManager.java:215)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.getDOM(TransformerImpl.java:556)
... 22 more
看到unknown protocol: classpath
错误时发现是我的sqlmap DTD文件找不到,我的sqlmap一开始是这样的:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "classpath:com/ibatis/sqlmap/engine/builder/xml/sql-map-2.dtd" >
<sqlMap namespace="funUser">
...
</sqlMap>
其中DTDs直接引用的jar包里的文件,工具无法识别就会报错,只能将其去掉,然后就转换成功了
不要以为一个工具就完事了,它只是试图在复杂的工作开始之前提供一个良好的起点,减少一点转换成本
转换后的mapper文件可能仍有一堆无法转换的部分或者转换错误,以我的应用为例,当时转换完成后有下面几个问题:
很多sqlmap文件写了resultMap,而且还加了jdbcType,比如:
<resultMap id="resultMap" type="user">
<result property="id" column="id" jdbcType="BIGINT"/>
<result property="appName" column="app_name" jdbcType="VARCHAR"/>
<result property="displayName" column="display_name" jdbcType="VARCHAR"/>
<result property="avatar" column="avatar" jdbcType="VARCHAR"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="gmtCreate" column="gmt_create" jdbcType="DATETIME"/>
<result property="gmtModified" column="gmt_modified" jdbcType="DATETIME"/>
</resultMap>
转换工具保持原样,而mybatis的jdbcType已经换了类型关键词,不再是BIGINT啊这些,最好方法就是去掉,一个文件就有这么多的字段要改,20个文件岂不是要手动改几百次,当然用脚本了:
sed -i '' 's/ jdbcType.*\/\>/\/\>/g' ./*.xml
注意:mybatis有两种字段映射的方式:select别名和resultmap方式,详细可看:mybatis – MyBatis 3 | Mapper XML 文件
转换后的mapper文件去掉了无法转换的\元素,因为mybatis里<typeAlias>
必须从<sqlMap>
元素移动到 <configuration><typeAliases>...</typeAliases></configuration>
这个里面,需要手动加个mybatis-config.xml
文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias alias="app" type="com.tmall.wireless.fun.core.app.AppDO" />
...
<typeAlias alias="punish" type="com.tmall.wireless.fun.core.punish.PunishDO"/>
</typeAliases>
</configuration>
虽然上面工具能转换大部分动态sql比如<isNotNull>
和<iterate>
,但像<isNotEmpty>
这样的动态sql是不支持的,需要手动转换
该工具还是比较强大的,可以将你的动态sql转换成功,比如<isNotNull.*?property=\"(.*?)\"></isNotNull>
改为:<if test="$1 != null"></if>
,<iterate>
标签改为<foreach>
标签,但免不了可能存在连接符丢失的情况,比如我有一条sql转换后少了一个and
ibatis使用spring封装好的SqlMapClientTemplate进行DAO类的增删改查,我们只需要继承SpringFramework中提供的SqlMapClientDaoSupport类即可
/**
* 把sql map client 注进来
*
* @author 竹敏 2016.3.25
*/
@SuppressWarnings("deprecation")
public class AutowiringSqlMapClientDaoSupport extends SqlMapClientDaoSupport {
@Resource
public void injectSqlMapClient(SqlMapClient sqlMapClient) {
super.setSqlMapClient(sqlMapClient);
}
}
然后通过getSqlMapClientTemplate()
即可进行select/insert/update/delete
操作
public class AppDAO extends AutowiringSqlMapClientDaoSupport {
public AppDO selectAppByName(String appName) {
return (AppDO) getSqlMapClientTemplate().queryForObject(
"funApp.selectByName", appName);
}
...
}
而mybatis采用的是SqlSessionTemplate进行操作,上面的类就要改成:
public class AppDAO {
@Resource
private SqlSessionTemplate sqlSessionTemplate;
public AppDO selectAppByName(String appName) {
return (AppDO) sqlSessionTemplate.selectOne("funApp.selectByName", appName);
}
...
}
想想如果有20个文件(大工程远不止),每个文件都要如此改,和完全重写工作量差不多,有两种方案可改善:
很简单,就是写个适配原来ibatis的DAO操作,接口如下:
/**
* Created by jinshuan.li on 2016/8/15.
* 适配ibatis的sqlmaptemplete
*/
public interface SqlMapClientAdapter {
SqlSession getSqlSession();
int insert(String statement, Object params);
<T> T queryForObject(String statement, Object params);
int update(String statement, Object params);
<T> List<T> queryForList(String statement, Object params);
<T> List<T> queryForList(String statement);
void delete(String statement, Object params);
<T> T queryForObject(String statement);
}
在适配器的实现类中调用SqlSession的相关方法:
**
* Created by jinshuan.li on 2016/8/15.
*/
@Repository("sqlSessionMapClient")
public class SqlSessionMapClient implements SqlMapClientAdapter {
@Resource(name = "sqlSessionTemplate")
private SqlSession sqlSession;
@Override
public SqlSession getSqlSession() {
return sqlSession;
}
@Override
public int insert(String statement, Object params) {
return sqlSession.insert(statement, params);
}
@Override
public <T> T queryForObject(String statement, Object params) {
return sqlSession.selectOne(statement, params);
}
@Override
public int update(String statement, Object params) {
return sqlSession.update(statement, params);
}
@Override
public <T> List<T> queryForList(String statement, Object params) {
return sqlSession.selectList(statement, params);
}
@Override
public <T> List<T> queryForList(String statement) {
return sqlSession.selectList(statement);
}
@Override
public void delete(String statement, Object params) {
sqlSession.delete(statement, params);
}
@Override
public <T> T queryForObject(String statement) {
return sqlSession.selectOne(statement);
}
}
适配器方式在大多数情况下都是优选方案,值得推荐,但免不了仍到处到是ibatis的影子,强迫症患者可能不想忍,要改就改彻底了,绝不留下ibatis的影子,那怎么办尼?只能手动写脚本批量改了,熟悉脚本的人很快就能写好,每个人都需要根据自己代码情况进行定制脚本,比如我的如下:
#### ibatis2mybatis_dao.sh
# 把getSqlMapClientTemplate()改成sqlSessionTemplate
find . -name "*DAO\.java" | xargs perl -pi -e 's|getSqlMapClientTemplate\(\)|sqlSessionTemplate|g'
# 方法名称一一对应改掉,queryForObject——>selectOne,queryForList——>selectList
find . -name "*DAO\.java" | xargs perl -pi -e 's|queryForObject|selectOne|g'
find . -name "*DAO\.java" | xargs perl -pi -e 's|queryForList|selectList|g'
find . -name "*DAO\.java" | xargs perl -pi -e 's|return[\s]*sqlSessionTemplate [\s]*\.selectList|return sqlSessionTemplate\.selectList|g'
# 去掉不再使用的AutowiringSqlMapClientDaoSupport辅助类
find . -name "*DAO\.java" | xargs perl -pi -e 's|import com\.tmall\.wireless\.fun\.core\.utils\.AutowiringSqlMapClientDaoSupport;||g'
find . -name "*DAO\.java" | xargs perl -pi -e 's| extends AutowiringSqlMapClientDaoSupport||g'
find . -name "*DAO\.java" | xargs sed -i '' '/@SuppressWarnings/d'
# 添加新的SqlSessionTemplate注入
find . -name "*DAO\.java" | xargs sed -i '' '/public class/a\
@Resource'
find . -name "*DAO\.java" | xargs sed -i '' '/@Resource/G'
find . -name "*DAO\.java" | xargs sed -i '' '/@Resource/a\
private SqlSessionTemplate sqlSessionTemplate;'
find . -name "*DAO\.java" | xargs sed -i '' '/public class/G'
find . -name "*DAO\.java" | xargs sed -i '' 's/^@Resource/ @Resource/'
find . -name "*DAO\.java" | xargs sed -i '' 's/^private/ private^/'
还有一些微调的部分,不适合用脚本,比如mybatis的insert方法已经不再返回新的id了,而是需要我们调用成功后直接从对象参数里获取,这里可能需要手动调整下
上面说了ibatis使用的是SqlMapClientTemplate对象,dataSource配置如下:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
"
default-autowire="byName">
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="dataSource" class="com.taobao.tddl.jdbc.group.TGroupDataSource" init-method="init">
<property name="appName" value="TMALL_FUN_APP" />
<property name="dbGroupKey" value="TMALL_FUN_GROUP" />
</bean>
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocations">
<list>
<value>classpath*:dao/sqlMapConfig.xml</value>
</list>
</property>
</bean>
<bean id="sqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">
<property name="sqlMapClient" ref="sqlMapClient" />
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
而mybatis使用的是SqlSessionTemplate,数据源需要改动配置:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
"
default-autowire="byName">
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="dataSource" class="com.taobao.tddl.jdbc.group.TGroupDataSource" init-method="init">
<property name="appName" value="TMALL_FUN_APP" />
<property name="dbGroupKey" value="TMALL_FUN_GROUP" />
</bean>
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:dao/mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:dao/mapper/*.xml"/>
</bean>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactoryBean"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>