教你如何开发Mybatis的通用Mapper

本文档地址: 如何开发本身的通用Mapperjava

博客排版不如直接在gitosc上查看,建议去上面的连接查看。git

#前言github

自从发了通用Mapper-0.1.0版本后,我以为对少数人来讲,这多是他们正好须要的一个工具。至少目前的通用DAO中,不多能有比这个更强大的。sql

可是对另外一部分人来讲,使用Mybatis代码生成器(我正在和一些朋友翻译这个文档,地址:MyBatis Generator)生成xml很方便,不须要使用通用Mapper。数据库

实际上若是你没法在本身的业务中提取出通用的单表(多表实际上能实现,可是限制会增多,不如手写xml)操做,通用的Mapper除了能增长你的初始效率以及更干净的xml配置外,没有特别大的优点。apache

为了更方便的扩展通用Mapper,我对0.1.0版本进行了重构。目前已经发布了0.2.0版本,这里要讲如何开发本身须要的通用Mapper。app

#如何开发本身的通用Mapperdom

##要求ide

  1. 本身定义的通用Mapper必须包含泛型,例如MysqlMapper<T>工具

  2. 自定义的通用Mapper接口中的方法须要有合适的注解。具体能够参考Mapper

  3. 须要继承MapperTemplate来实现具体的操做方法。

  4. 通用Mapper中的Provider一类的注解只能使用相同的type类型(这个类型就是第三个要实现的类。)。实际上method也都写的同样。

##HsqldbMapper实例

###第一步,建立HsqldbMapper<T>

public interface HsqldbMapper<T> {
}

这个接口就是咱们定义的通用Mapper,具体的接口方法在第三步写。其余的Mapper能够继承这个HsqldbMapper<T>

###第二部,建立HsqldbProvider

public class HsqldbProvider extends MapperTemplate {
    //继承父类的方法
    public HsqldbProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
        super(mapperClass, mapperHelper);
    }
}

这个类是实际处理操做的类,须要继承MapperTemplate,具体代码在第四步写。

###第三步,在HsqldbMapper<T>中添加通用方法 这里以一个分页查询做为例子。 public interface HsqldbMapper<T> { /** * 单表分页查询 * * @param object * @param offset * @param limit * @return */ @SelectProvider(type=HsqldbProvider.class,method = "dynamicSQL") List<T> selectPage(@Param("entity") T object, @Param("offset") int offset, @Param("limit") int limit); }

返回结果为List<T>,入参分别为查询条件和分页参数。在Mapper的接口方法中,当有多个入参的时候建议增长@Param注解,不然就得用param1,param2...来引用参数。

同时必须在方法上添加注解。查询使用SelectProvider,插入使用@InsertProvider,更新使用UpdateProvider,删除使用DeleteProvider。不一样的Provider就至关于xml中不一样的节点,如<select>,<insert>,<update>,<delete>

由于这里是查询,因此要设置为SelectProvider,这4个Provider中的参数都同样,只有typemethod

type必须设置为实际执行方法的HasqldbProvider.class,method必须设置为"dynamicSQL"

通用Mapper处理的时候会根据type反射HasqldbProvider查找方法,而Mybatis的处理机制要求method必须是type类中只有一个入参,且返回值为String的方法。"dynamicSQL"方法定义在MapperTemplate中,该方法以下:

public String dynamicSQL(Object record) {
    return "dynamicSQL";
}

这个方法只是为了知足Mybatis的要求,没有任何实际的做用。

###第四步,在HsqldbProvider中实现真正处理Sql的方法

在这里有一点要求,那就是HsqldbProvider处理HsqldbMapper<T>中的方法时,方法名必须同样,由于这里须要经过反射来获取对应的方法,方法名一致一方面是为了减小开发人员的配置,另外一方面和接口对应看起来更清晰。

除了方法名必须同样外,入参必须是MappedStatement ms,除此以外返回值能够是void或者SqlNode之一。

这里先讲一下通用Mapper的实现原理。通用Mapper目前是经过拦截器在通用方法第一次执行的时候去修改MappedStatement对象的SqlSource属性。并且只会执行一次,之后就和正常的方法没有任何区别。

使用Provider注解的这个Mapper方法,Mybatis自己会处理成ProviderSqlSource(一个SqlSource的实现类),因为以前的配置,这个ProviderSqlSource种的SQL是上面代码中返回的"dynamicSQL"。这个SQL没有任何做用,若是不作任何修改,执行这个代码确定会出错。因此在拦截器中拦截符合要求的接口方法,遇到ProviderSqlSource就经过反射调用如HsqldbProvider中的具体代码去修改原有的SqlSource

最简单的处理Mybatis SQL的方法是什么?就是建立SqlNode,使用DynamicSqlSource,这种状况下咱们不须要处理入参,不须要处理代码中的各类类型的参数映射。比执行SQL的方式容易不少。

有关这部分的内容建议查看通用Mapper的源码和Mybatis源码了解,若是不了解在这儿说多了反而会乱。

下面在HsqldbProvider中添加public SqlNode selectPage(MappedStatement ms)方法:

/**
 * 分页查询
 * @param ms
 * @return
 */
public SqlNode selectPage(MappedStatement ms) {
    Class<?> entityClass = getSelectReturnType(ms);
    //修改返回值类型为实体类型
    setResultType(ms, entityClass);

    List<SqlNode> sqlNodes = new ArrayList<SqlNode>();
    //静态的sql部分:select column ... from table
    sqlNodes.add(new StaticTextSqlNode("SELECT "
            + EntityHelper.getSelectColumns(entityClass)
            + " FROM "
            + tableName(entityClass)));
    //获取所有列
    List<EntityHelper.EntityColumn> columnList = EntityHelper.getColumns(entityClass);
    List<SqlNode> ifNodes = new ArrayList<SqlNode>();
    boolean first = true;
    //对全部列循环,生成<if test="property!=null">[AND] column = #{property}</if>
    for (EntityHelper.EntityColumn column : columnList) {
        StaticTextSqlNode columnNode
                = new StaticTextSqlNode((first ? "" : " AND ") + column.getColumn()
						 + " = #{entity." + column.getProperty() + "} ");
        if (column.getJavaType().equals(String.class)) {
            ifNodes.add(new IfSqlNode(columnNode, "entity."+column.getProperty()
						 + " != null and " + "entity."+column.getProperty() + " != '' "));
        } else {
            ifNodes.add(new IfSqlNode(columnNode, "entity."+column.getProperty() + " != null "));
        }
        first = false;
    }
    //将if添加到<where>
    sqlNodes.add(new WhereSqlNode(ms.getConfiguration(), new MixedSqlNode(ifNodes)));
    //处理分页
    sqlNodes.add(new IfSqlNode(new StaticTextSqlNode(" LIMIT #{limit}"),"offset==0"));
    sqlNodes.add(new IfSqlNode(new StaticTextSqlNode(" LIMIT #{limit} OFFSET #{offset} "),"offset>0"));
    return new MixedSqlNode(sqlNodes);
}

注:对这段代码感受吃力的,能够对比本页最下面结构部分XML形式的查看。

首先这段代码要实现的功能是这样,根据传入的实体类参数中不等于null(字符串也不等于'')的属性做为查询条件进行查询,根据分页参数进行分页。

先看这两行代码:

//获取实体类型
Class<?> entityClass = getSelectReturnType(ms);
//修改返回值类型为实体类型
setResultType(ms, entityClass);

首先获取了实体类型,而后经过setResultType将返回值类型改成entityClass,就至关于resultType=entityClass

**这里为何要修改呢?**由于默认返回值是T,Java并不会自动处理成咱们的实体类,默认状况下是Object,对于全部的查询来讲,咱们都须要手动设置返回值类型。

对于insert,update,delete来讲,这些操做的返回值都是int,因此不须要修改返回结果类型。

以后从List<SqlNode> sqlNodes = new ArrayList<SqlNode>();代码开始拼写SQL,首先是SELECT查询头,在EntityHelper.getSelectColumns(entityClass)中还处理了别名的状况。

而后获取全部的列,对列循环建立<if entity.property!=null>column = #{entity.property}</if>节点。最后把这些if节点组成的List放到一个<where>节点中。

这一段使用属性时用的是 entity. + 属性名entity来自哪儿?来自咱们前面接口定义处的Param("entity")注解,后面的两个分页参数也是。若是你用过Mybatis,相信你能明白。

以后在<where>节点后添加分页参数,当offset==0时和offset>0时的分页代码不一样。

最后封装成一个MixedSqlNode返回。

返回后通用Mapper是怎么处理的,这里贴下源码:

SqlNode sqlNode = (SqlNode) method.invoke(this, ms);
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(ms.getConfiguration(), sqlNode);
setSqlSource(ms, dynamicSqlSource);

返回SqlNode后建立了DynamicSqlSource,而后修改了ms原来的SqlSource

###第五步,配置通用Mapper接口到拦截器插件中

<plugins>
	<plugin interceptor="com.github.abel533.mapper.MapperInterceptor">
		<!--================================================-->
		<!--可配置参数说明(通常无需修改)-->
		<!--================================================-->
		<!--UUID生成策略-->
		<!--配置UUID生成策略须要使用OGNL表达式-->
		<!--默认值32位长度:@java.util.UUID@randomUUID().toString().replace("-", "")-->
		<!--<property name="UUID" value="@java.util.UUID@randomUUID().toString()"/>-->
		<!--主键自增回写方法,默认值MYSQL,详细说明请看文档-->
		<property name="IDENTITY" value="HSQLDB"/>
  <!--序列的获取规则,使用{num}格式化参数,默认值为{0}.nextval,针对Oracle-->
  <!--可选参数一共3个,对应0,1,2,分别为SequenceName,ColumnName,PropertyName-->
		<property name="seqFormat" value="{0}.nextval"/>
		<!--主键自增回写方法执行顺序,默认AFTER,可选值为(BEFORE|AFTER)-->
		<!--<property name="ORDER" value="AFTER"/>-->
  <!--支持Map类型的实体类,自动将大写下划线的Key转换为驼峰式-->
  <!--这个处理使得通用Mapper能够支持Map类型的实体(实体中的字段必须按常规方式定义,不然没法反射得到列)-->
  <property name="cameHumpMap" value="true"/>
		<!--通用Mapper接口,多个用逗号隔开-->
		<property name="mappers" value="com.github.abel533.mapper.Mapper,com.github.abel533.hsqldb.HsqldbMapper"/>
	</plugin>
</plugins>

这里主要是mappers参数:

<property name="mappers" value="com.github.abel533.mapper.Mapper,com.github.abel533.hsqldb.HsqldbMapper"/>

多个通用Mapper能够用逗号隔开。

##测试

接下来编写代码进行测试。

public interface CountryMapper extends Mapper<Country>,HsqldbMapper<Country> {
}

CountryMapper上增长继承HsqldbMapper<Country>

编写以下的测试:

@Test
public void testDynamicSelectPage() {
    SqlSession sqlSession = MybatisHelper.getSqlSession();
    try {
        CountryMapper mapper = sqlSession.getMapper(CountryMapper.class);
        //带查询条件的分页查询
        Country country = new Country();
        country.setCountrycode("US");
        List<Country> countryList = mapper.selectPage(country, 0, 10);
        //查询总数
        Assert.assertEquals(1, countryList.size());
        //空参数的查询
        countryList = mapper.selectPage(new Country(), 100, 10);
        Assert.assertEquals(10, countryList.size());
    } finally {
        sqlSession.close();
    }
}

测试输出日志以下:

DEBUG [main] - ==>  Preparing: SELECT ID,COUNTRYNAME,COUNTRYCODE FROM COUNTRY WHERE COUNTRYCODE = ? LIMIT ? 
DEBUG [main] - ==> Parameters: US(String), 10(Integer)
TRACE [main] - <==    Columns: ID, COUNTRYNAME, COUNTRYCODE
TRACE [main] - <==        Row: 174, United States of America, US
DEBUG [main] - <==      Total: 1
DEBUG [main] - ==>  Preparing: SELECT ID,COUNTRYNAME,COUNTRYCODE FROM COUNTRY LIMIT ? OFFSET ? 
DEBUG [main] - ==> Parameters: 10(Integer), 100(Integer)
TRACE [main] - <==    Columns: ID, COUNTRYNAME, COUNTRYCODE
TRACE [main] - <==        Row: 101, Maldives, MV
TRACE [main] - <==        Row: 102, Mali, ML
TRACE [main] - <==        Row: 103, Malta, MT
TRACE [main] - <==        Row: 104, Mauritius, MU
TRACE [main] - <==        Row: 105, Mexico, MX
TRACE [main] - <==        Row: 106, Moldova, Republic of, MD
TRACE [main] - <==        Row: 107, Monaco, MC
TRACE [main] - <==        Row: 108, Mongolia, MN
TRACE [main] - <==        Row: 109, Montserrat Is, MS
TRACE [main] - <==        Row: 110, Morocco, MA
DEBUG [main] - <==      Total: 10

测试没有任何问题。

这里在来点很容易实现的一个功能。上面代码中:

countryList = mapper.selectPage(new Country(), 100, 10);

传入一个没有设置任何属性的Country的时候会查询所有结果。有些人会以为传入一个空的对象不如传入一个null。咱们修改测试代码看看结果。

执行测试代码后抛出异常:

Caused by: org.apache.ibatis.ognl.OgnlException: source is null for getProperty(null, "id")

为何会异常呢,由于咱们上面代码中直接引用的entity.property,在引用前并无判断entity != null,于是致使了这里的问题。

咱们修改HsqldbProvider中的selectPage方法,将最后几行代码进行修改,原来的代码:

//将if添加到<where>
sqlNodes.add(new WhereSqlNode(ms.getConfiguration(), new MixedSqlNode(ifNodes)));

修改后:

//增长entity!=null判断
IfSqlNode ifSqlNode = new IfSqlNode(new MixedSqlNode(ifNodes),"entity!=null");
//将if添加到<where>
sqlNodes.add(new WhereSqlNode(ms.getConfiguration(), ifSqlNode));

以后再进行测试就没有问题了。

##更多例子

更多例子能够参考通用Mapper中的Mapper<T>MapperProvider进行参考。代码量不是很大可是实现了经常使用的这些功能。

当你了解了原理以及掌握了SqlNode的结构后,相信你能写出更多更强大的通用Mapper。

我曾经说过会根据不一样的数据库写一些针对性的通用Mapper,当我开始考虑重构的时候,我就想,我应该教会须要这个插件的开发人员如何本身实现。

一我的的能力是有限的,并且写一个东西开源出来给你们用很容易,可是维护不易。因此呢,我但愿以为这篇文档有用的各位可以分享本身的实现。

我我的若是有时间,我会考虑增长通用的Example查询。Example类的设计比较复杂,对应的SqlNode结构并非很复杂。若是有人有兴趣,我能够协助开发Example通用查询。

##结构

对于刚刚了解上述内容的开发人员来讲,SqlNode可能没有那么直观,为了便于理解。我在这里将上面最后修改完成的SqlNode以xml的形式写出来。

<select id="selectPage" resultType="com.github.abel533.model.Country">
	SELECT ID,COUNTRYNAME,COUNTRYCODE FROM COUNTRY
	<where>
		<if test="entity!=null>
			<if test="entity.id!=null">
				id = #{entity.id}
			</if>
			<if test="entity.countryname!=null and entity.countryname!=''">
				countryname = #{entity.countryname}
			</if>
			<if test="entity.countrycode!=null and entity.countrycode!=''">
				countrycode = #{entity.countrycode}
			</if>
		</if>
	</where>
	<if test="offset==0">
		LIMIT #{limit}
	</if>
	<if test="offset>0">
		LIMIT #{limit} OFFSET #{offset}
	</if>
</select>

看到这个结构,再和上面代码一一对应应该就不难理解了。熟悉之后,你可能也会以为JAVA代码方式处理通用的Mapper会容易不少。

相关文章
相关标签/搜索