Mybatis中SqlMapper配置的扩展与应用(1)

奋斗了好几个晚上调试程序,写了好几篇博客,终于建立起了Mybatis配置的扩展机制。虽然扩展机制是重要的,然而如果没有真正实用的扩展功能,那也至少是不那么鼓舞人心的,这篇博客就来举几个扩展的例子。

这次研读源码的起因是Oracle和MySQL数据库的兼容性,比如在Oracle中使用双竖线作为连接符,而MySQL中使用CONCAT函数;比如Oracle中可以使用DECODE函数,而MySQL中只能使用标准的CASE WHEN;又比如Oracle中可以执行DELETE FORM TABLE WHERE FIELD1 IN (SELECT FIELD1 FORM TABLE WHERE FIELD2=?),但是MySQL中会抛出异常,等等。下面就从解决这些兼容性问题开始,首先需要在配置中添加数据库标识相关的配置:

<!-- 自行构建Configuration对象 -->
<bean id="mybatisConfig" class="org.dysd.dao.mybatis.schema.SchemaConfiguration"/>
<bean id="sqlSessionFactory" p:dataSource-ref="dataSource"
    class="org.dysd.dao.mybatis.schema.SchemaSqlSessionFactoryBean">
    <!-- 注入mybatis配置对象 -->
    <property name="configuration" ref="mybatisConfig"/>
    <!-- 自动扫描SqlMapper配置文件 -->
    <property name="mapperLocations">
        <array>
            <value>classpath*:**/*.sqlmapper.xml</value>
        </array>
    </property>
    <!-- 数据库产品标识配置 -->
    <property name="databaseIdProvider">
        <bean class="org.apache.ibatis.mapping.VendorDatabaseIdProvider">
            <property name="properties">
                <props>
                    <!-- 意思是如果数据库产品描述中包含关键字MYSQL,则使用mysql作为Configuration中的databaseId,mybatis原生的实现关键字区分大小写,我没有测试Oracle和DB2 -->
                    <prop key="MySQL">mysql</prop>
                    <prop key="oracle">oracle</prop>
                    <prop key="H2">h2</prop>
                    <prop key="db2">db2</prop>
                </props>
            </property>
        </bean>
    </property>
</bean>

一、连接符问题

1、编写SQL配置函数实现类

public class ConcatSqlConfigFunction extends AbstractSqlConfigFunction{//抽象父类中设定了默认的order级别

    @Override
    public String getName() {
        return "concat";
    }

    @Override
    public String eval(String databaseId, String[] args) {
        if(args.length < 2){
            Throw.throwException("the concat function require at least two arguments.");
        }
        if("mysql".equalsIgnoreCase(databaseId)){
            return "CONCAT("+Tool.STRING.join(args, ",")+")";
        }else{
            return Tool.STRING.join(args, "||");
        }
    }
}

2、在SchemaHandlers类的静态代码块中注册,或者在启动初始化类中调用SchemaHandlers的方法注册

static {
    //注册默认命名空间的StatementHandler
    register("cache-ref", new CacheRefStatementHandler());
    register("cache", new CacheStatementHandler());
    register("parameterMap", new ParameterMapStatementHandler());
    register("resultMap", new ResultMapStatementHandler());
    register("sql", new SqlStatementHandler());
    register("select|insert|update|delete", new CRUDStatementHandler());

    //注册默认命名空间的ScriptHandler
    register("trim", new TrimScriptHandler());
    register("where", new WhereScriptHandler());
    register("set", new SetScriptHandler());
    register("foreach", new ForEachScriptHandler());
    register("if|when", new IfScriptHandler());
    register("choose", new ChooseScriptHandler());
    //register("when", new IfScriptHandler());
    register("otherwise", new OtherwiseScriptHandler());
    register("bind", new BindScriptHandler());

    // 注册自定义命名空间的处理器
    registerExtend("db", new DbStatementHandler(), new DbScriptHandler());

    // 注册SqlConfigFunction
    register(new DecodeSqlConfigFunction());
    register(new ConcatSqlConfigFunction());

    // 注册SqlConfigFunctionFactory
    register(new LikeSqlConfigFunctionFactory());
}

上面代码除了注册ConcatSQLConfigFunction外,还有一些其它的注册代码,这里一并给出,下文将省略。

3、修改SqlMapper配置

<select id="selectString" resultType="string">
    select PARAM_NAME, $concat{PARAM_CODE, PARAM_NAME} AS CODE_NAME
      from BF_PARAM_ENUM_DEF
    <if test="null != paramName and ‘‘ != paramName">
     where PARAM_NAME LIKE $CONCAT{‘%‘, #{paramName, jdbcType=VARCHAR}, ‘%‘}
    </if>
</select>

4、编写dao接口类

@Repository
public interface IExampleDao {

    public String selectString(@Param("paramName")String paramName);
}

5、编写测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={
    "classpath:spring/applicationContext.xml"
})
@Component
public class ExampleDaoTest {

    @Resource
    private IExampleDao dao;

    @Test
    public void testSelectString(){
        String a = dao.selectString("显示");
        Assert.assertEquals("显示区域", a);
    }
}

6、分别在MySQL和H2中运行如下(将mybatis日志级别调整为TRACE)

(1)MySQL

20161108 00:12:55,235 [main]-[DEBUG] ==>  Preparing: select PARAM_NAME, CONCAT(PARAM_CODE,PARAM_NAME) AS CODE_NAME from BF_PARAM_ENUM_DEF where PARAM_NAME LIKE CONCAT(‘%‘,?,‘%‘)
20161108 00:12:55,269 [main]-[DEBUG] ==> Parameters: 显示(String)
20161108 00:12:55,287 [main]-[TRACE] <==    Columns: PARAM_NAME, CODE_NAME
20161108 00:12:55,287 [main]-[TRACE] <==        Row: 显示区域, DISPLAY_AREA显示区域
20161108 00:12:55,289 [main]-[DEBUG] <==      Total: 1

(2)H2

20161108 00:23:08,348 [main]-[DEBUG] ==>  Preparing: select PARAM_NAME, PARAM_CODE||PARAM_NAME AS CODE_NAME from BF_PARAM_ENUM_DEF where PARAM_NAME LIKE ‘%‘||?||‘%‘
20161108 00:23:08,364 [main]-[DEBUG] ==> Parameters: 显示(String)
20161108 00:23:08,411 [main]-[TRACE] <==    Columns: PARAM_NAME, CODE_NAME
20161108 00:23:08,411 [main]-[TRACE] <==        Row: 显示区域, DISPLAY_AREA显示区域
20161108 00:23:08,411 [main]-[DEBUG] <==      Total: 1

可以看到,已经解决连接符的兼容性问题了。

另外,我们也发现,使用LIKE关键字时,写起来比较麻烦,那我们就给它一组新的SQL配置函数吧:

public class LikeSqlConfigFunctionFactory implements ISqlConfigFunctionFactory{

    @Override
    public Collection<ISqlConfigFunction> getSqlConfigFunctions() {
        return Arrays.asList(getLeftLikeSqlConfigFunction(),getRightLikeSqlConfigFunction(),getLikeSqlConfigFunction());
    }

    private ISqlConfigFunction getLeftLikeSqlConfigFunction(){
        return new AbstractLikeSqlConfigFunction(){
            @Override
            public String getName() {
                return "llike";
            }

            @Override
            protected String eval(String arg) {
                return "LIKE $concat{‘%‘,"+arg+"}";
            }
        };
    }

    private ISqlConfigFunction getRightLikeSqlConfigFunction(){
        return new AbstractLikeSqlConfigFunction(){
            @Override
            public String getName() {
                return "rlike";
            }

            @Override
            protected String eval(String arg) {
                return "LIKE $concat{"+arg+", ‘%‘}";
            }
        };
    }

    private ISqlConfigFunction getLikeSqlConfigFunction(){
        return new AbstractLikeSqlConfigFunction(){
            @Override
            public String getName() {
                return "like";
            }

            @Override
            protected String eval(String arg) {
                return "LIKE $concat{‘%‘,"+arg+", ‘%‘}";
            }
        };
    }

    private abstract class AbstractLikeSqlConfigFunction extends AbstractSqlConfigFunction{
        @Override
        public String eval(String databaseId, String[] args) {
            if(args.length != 1){
                Throw.throwException("the like function require one and only one argument.");
            }
            return eval(args[0]);
        }
        protected abstract String eval(String arg);
    }
}

这里,定义了一组SQL配置函数,左相似,右相似以及中间相似匹配,并且SQL配置函数还可以嵌套。于是,SqlMapper的配置文件简化为:

<select id="selectString" resultType="string">
    select PARAM_NAME, $concat{PARAM_CODE, PARAM_NAME} AS CODE_NAME
      from BF_PARAM_ENUM_DEF
    <if test="null != paramName and ‘‘ != paramName">
     where PARAM_NAME $like{#{paramName, jdbcType=VARCHAR}}
    </if>
</select>

运行结果完全相同。

如果还觉得麻烦,因为PARAM_NAME和paramName是驼峰式对应,甚至还可以添加一个fieldLike函数,并将配置修改为

where  $fieldLike{#{PARAM_NAME, jdbcType=VARCHAR}}

如果再结合数据字典,jdbcType的配置也可自动生成:

where  $fieldLike{#{PARAM_NAME}}

这种情形下,如果有多个参数,也不会出现歧义(或者新定义一个配置函数$likes{}消除歧义),于是可将多个条件简化成:

where  $likes{#{PARAM_NAME, PARAM_NAME2, PARAM_NAME3}}

当然,还有更多可挖掘的简化,已经不止是兼容性的范畴了,这里就不再进一步展开了。

二、DECODE函数/CASE ... WHEN

Oracle中的DECODE函数非常方便,语法如下:

DECODE(条件,值1,返回值1,值2,返回值2,...值n,返回值n[,缺省值])

等价的标准写法:

CASE 条件
  WHEN 值1 THEN 返回值1
  WHEN 值2 THEN 返回值2
  ...
  WHEN 值n THEN 返回值n
 [ELSE 缺省值]
END

现在我们来实现一个$decode配置函数:

public class DecodeSqlConfigFunction extends AbstractSqlConfigFunction{

    @Override
    public String getName() {
        return "decode";
    }

    @Override
    public String eval(String databaseId, String[] args) {
        if(args.length < 3){
            Throw.throwException("the decode function require at least three arguments.");
        }
        if("h2".equalsIgnoreCase(databaseId)){//测试时,使用h2代替oracle,正式程序中修改为oracle
            return "DECODE("+Tool.STRING.join(args, ",")+")";
        }else{
            StringBuffer sb = new StringBuffer();
            sb.append("CASE ").append(args[0]);
            int i=2, l = args.length;
            for(; i < l; i= i+2){
                sb.append(" WHEN ").append(args[i-1]).append(" THEN ").append(args[i]);
            }
            if(i == l){//结束循环时,两者相等说明最后一个参数未使用
                sb.append(" ELSE ").append(args[l-1]);
            }
            sb.append(" END");
            return sb.toString();
        }
    }
}

然后使用SchemaHandlers注册,修改SqlMapper中配置:

<select id="selectString" resultType="string">
    select PARAM_NAME, $decode{#{paramName}, ‘1‘, ‘A‘, ‘2‘, ‘B‘,‘C‘} AS DECODE_TEST
      from BF_PARAM_ENUM_DEF
    <if test="null != paramName and ‘‘ != paramName">
     where PARAM_NAME $like{#{paramName, jdbcType=VARCHAR}}
    </if>
</select>

测试如下:

(1)H2中(以H2代替Oracle)

20161108 06:53:29,747 [main]-[DEBUG] ==>  Preparing: select PARAM_NAME, DECODE(?,‘1‘,‘A‘,‘2‘,‘B‘,‘C‘) AS DECODE_TEST from BF_PARAM_ENUM_DEF where PARAM_NAME LIKE ‘%‘||?||‘%‘ 

(2)MySQL中

20161108 06:50:55,998 [main]-[DEBUG] ==>  Preparing: select PARAM_NAME, CASE ? WHEN ‘1‘ THEN ‘A‘ WHEN ‘2‘ THEN ‘B‘ ELSE ‘C‘ END AS DECODE_TEST from BF_PARAM_ENUM_DEF where PARAM_NAME LIKE ‘%‘||?||‘%‘ 
时间: 2024-08-10 15:11:08

Mybatis中SqlMapper配置的扩展与应用(1)的相关文章

MyBatis中Mapper配置常见问题(持续更新)

入参为List时,foreach标签中collection属性值固定为"list"(list存放在map中作为入参时,collection应为key值): <foreach item="item" collection="list" open="(" separator="," close=")"> #{item, jdbcType=VARCHAR, javaType=str

优化与扩展Mybatis的SqlMapper解析

接上一篇博文,这一篇来讲述怎么实现SchemaSqlMapperParserDelegate——解析SqlMapper配置文件. 要想实现SqlMapper文件的解析,还需要仔细分析一下mybatis的源码,我画了一个图来协助理解,也可以帮助形成一个整体概念: 当然,这幅图不止是原生的解析,也包括了XSD模式下的解析,下面对着这幅图来说明一下. 一.Mybatis全局配置 Mybatis的全局配置,对应内存对象为Configuration,是重量级对象,和数据源DataSource.会话工厂Sq

MyBatis学习总结(三)——优化MyBatis配置文件中的配置(转载)

孤傲苍狼 只为成功找方法,不为失败找借口! MyBatis学习总结(三)--优化MyBatis配置文件中的配置 一.连接数据库的配置单独放在一个properties文件中 之前,我们是直接将数据库的连接配置信息写在了MyBatis的conf.xml文件中,如下: 1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org

【转】MyBatis学习总结(三)——优化MyBatis配置文件中的配置

[转]MyBatis学习总结(三)——优化MyBatis配置文件中的配置 一.连接数据库的配置单独放在一个properties文件中 之前,我们是直接将数据库的连接配置信息写在了MyBatis的conf.xml文件中,如下: 1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//E

Mybatis中配置Mapper的方法

Mybatis中配置Mapper的方法 在这篇文章中我主要想讲一下Mybatis配置文件中mappers元素的配置.关于基础部分的内容可以参考http://blog.csdn.net/elim168/article/details/40622491. 我们知道在Mybatis中定义Mapper信息有两种方式,一种是利用xml写一个对应的包含Mapper信息的配置文件:另一种就是定义一个Mapper接口,然后定义一些相应的操作方法,再辅以相应的操作注解. 现假设我有这样一个实体类: Java代码 

【MyBatis学习05】SqlMapConfig.xml文件中的配置总结

经过上两篇博文的总结,对mybatis中的dao开发方法和流程基本掌握了,这一节主要来总结一下mybatis中的全局配置文件SqlMapConfig.xml在开发中的一些常用配置,首先看一下该全局配置文件中都有哪些可以配置的东西: 配置内容 作用 <properties> 用来加载属性文件 <settings> 用来设置全局参数 <typeAliases> 用来设置类型的别名 <typeHandlers> 用来设置类型处理器 <objectFactor

Mybatis中接口和对应的mapper文件位置配置详解

Mybatis中接口和对应的mapper文件位置配置详解 原链接为:https://blog.csdn.net/fanfanzk1314/article/details/71480954 今天遇到一个问题是mybatis中接口和对应的mapper文件位置不同,而引起的操作也会不同,在网上找了好久最终找到了方法,这里就简单的解析一下: 我们知道在典型的maven工程中,目录结构有:src/main/java和src/main/resources,前者是用来存放java源代码的,后者则是存放一些资源

使用XSD校验Mybatis的SqlMapper配置文件(1)

这篇文章以前面对SqlSessionFactoryBean的重构为基础,先简单回顾一下做了哪些操作: 新建SqlSessionFactoryBean,初始代码和mybatis-spring相同: 重构buildSqlSessionFactory()方法,将众多的if语句抽取为一组小方法,预留自定义配置的方法,同时添加常用属性的getter方法: 提取组件工厂接口,并提供组件创建工具类SqlSessionComponetFactorys,将散落在不同地方的new Xxx()集中管理,便于组件的替换

使用XSD校验Mybatis的SqlMapper配置文件(2)

编写好XSD文件,然后来看怎么使用XSD文件校验,并解析SqlMapper文件,也就是实现doParseSqlMapperResourceWithSchema()方法. 为了实现这个功能,有两个基本要求: (1)兼容性:需要兼容mybatis的原生配置,兼容有两种层级,一种是使用DTD校验,这个前面已经说了,走原来的流程,兼容性没有问题:另一种就是走XSD校验,但也需要兼容mybatis原生配置,这种兼容性一方面从上面修改的XSD文件去保证,另一方面也需要从XML的解析去保证. (2)扩展性:修