掘金 后端 ( ) • 2024-05-13 10:28

theme: smartblue

前言

在看了别人的项目后,构思自己的通用Mapper,想法是类似于mybatis-plus使用继承的方式添加日常的增删改查功能。例如下面的使用方式:

public interface UserMapper extends CommonMapper<User> {}

因为还在学习,边学习边敲代码进行验证的,写文章是为了记录学习和踩坑的地方,文章和学习的进度是同步的。如果有不合理的和能改进的,还望各位读者谅解和指出。

mybatis-provider

搜索教程,看到mybatis3加入了几个provier注解,分别是:@SelectProvider,@InsertProvider,@UpdateProvider,@DeleteProvider,其中文档说明允许构建动态SQL,这意味着我们可以基于此类注解实现动态增删改查SQL的功能。

image.png

基础使用示例

展示单个/多个参数的基础实现

public interface UserMapper {

    User selectUser(String id);

    @SelectProvider(type = UserSelectProvider.class,method = "selectUserByEmail")
    User selectUserByEmail(@Param("email") String email);

    @SelectProvider(type = UserSelectProvider.class, method = "selectUserByName")
    User selectUserByName(@Param("name") String name, @Param("orderByColum")String orderByColum);
}
package org.nott.web.provider;

import org.apache.ibatis.jdbc.SQL;

public class UserSelectProvider {

    public String selectUserByEmail(String email) {
        return new SQL() {{
            SELECT("*");
            FROM("user");
            WHERE("email like #{email}");
        }}.toString();
    }
    
    public String selectUserByName(String name,String orderByColum){
        return new SQL(){{
            SELECT("*");
            FROM("user");
            WHERE("name like #{name}");
            ORDER_BY(orderByColum);
        }}.toString();
    }
}

注:3.5.6版本后可以指定模板defaultSqlProviderType,省略指定xxProvider注解中的type/value,id

单元测试

junit test method

编写CommonProvider

根据上述例子,可以动态构建SQL,只需指定SQL中可变的字段和条件,实现通用的CommonMapper提供出通用的增删改查方法。我没有使用defaultSqlProviderType,而是将上述四个provider注解分别供四个provider进行选择。

条件构造

看到上面mybatis提供的例子,想到应该写一个条件构造,最简单的需要动态的建立select条件,这里定义了一个条件构造SimpleSqlConditionBuilder,继承的SqlQuery只是用来定义里面的条件方法内容。 先写了等于和不等于进行下面简单的select方法的测试。

@Data
public class SimpleSqlConditionBuilder implements SqlQuery{

    List<SqlConditions> sqlConditions = new ArrayList<>();

    Integer limit;


    public static SimpleSqlConditionBuilder create(Class tClass){
        return new SimpleSqlConditionBuilder();
    }

    @Override
    public SimpleSqlConditionBuilder eq(String colum, Object val) {
        SqlConditions conditions = new SqlConditions(colum,val,SqlOperator.EQ);
        this.sqlConditions.add(conditions);
        return this;
    }

    @Override
    public SimpleSqlConditionBuilder neq(String colum, Object val) {
        SqlConditions conditions = new SqlConditions(colum,val,SqlOperator.NEQ);
        this.sqlConditions.add(conditions);
        return this;
    }


    public SimpleSqlConditionBuilder limit(Integer value){
        this.limit = value;
        return this;
    }

}
// 用于构建sql 条件 运算符 值 的运算条件
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SqlConditions {

    private String colum;

    private Object value;
    
    // 运算符枚举
    private SqlOperator sqlOperator;


}

BaseSelectProvider

这里定义了一个BaseSelectProvider供SelectProvider注解的type选中,内容是封装一般的查找方法,先个selectOne方法试试。(其中,BaseSelectProvider这里的泛型是有伏笔的,后面会踩坑)

public class BaseSelectProvider<T> {
   /**
     * 获取当前类的泛型
     */
    private Class genericSuperClass;

    public BaseSelectProvider(){
        Type genericSuperType = this.getClass().getGenericSuperclass();
        if(genericSuperType == null){
            throw new GenericClassException("Generic class not found");
        }
//        Type[] types = ((ParameterizedType) genericSuperType).getActualTypeArguments();

        Type[] p = ((ParameterizedType) genericSuperType).getActualTypeArguments();
        System.out.println(Arrays.toString(p));
        this.genericSuperClass = (Class) p[0];
    }}
    
    // 创建mybatis SQL对象,获得sql语句
    public String buildSql(SimpleSqlConditionBuilder simpleSqlConditionBuilder){
        // 使用当前范型class获取对象信息,表名等
        Class<?> aClass = this.genericSuperClass;
         Class<?> aClass = this.genericSuperClass;
        boolean annotationPresent = aClass.isAnnotationPresent(CustTableName.class);
        String tableName;
        if(annotationPresent){
            CustTableName custTableName = (CustTableName) aClass.getAnnotation(CustTableName.class);
            tableName = custTableName.name();
        }else {
            String simpleName = aClass.getSimpleName();
            // 大驼峰转下划线处理
            tableName = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, simpleName);
        }
        ...
    }
    
    public String selectOne(){
        SimpleSqlConditionBuilder.create(this.genericSuperClass.getClass()).limit(1);
        return buildSql(null);
    }

CommonMapper

这里就是我们提供给外面继承的CommonMapper,类似于mbatis-plus的BaseMapper

public interface CommonMapper<T> {
    
    @SelectProvider(type = BaseSelectProvider.class,method = "selectOne")
    public T selectOne();

}

测试(不通过)

web测试模块UserMapper继承CommonMapper

编写测试方法

image.png

这里理所当然地失败了,还记得CommonMapper里的方法上面的注解 @SelectProvider(type = BaseSelectProvider.class,method = "selectOne"),这里BaseSelectProvider压根不知道泛型是什么,所以它的泛型获取到是Object类,不能转换到CommonMapper上提供的<T>,所以报错了。

修改

后面我修改成传入T t对象作为参数,因此调用的时候每个方法都需要加入t对象,会显得很笨重,有点不合理。

image.png

小结

虽然后面新写的条件构造也通过单元测试,但是感觉还需要修改,BaseSelectProvider获取SQL感觉需要改成静态方法,不过今天是休息日,落笔时间已经是晚上十点,明天又是工作日,有点写不下去了。

下次找到别的解决方法再同步更新下一篇文章,顺便把根据主键操作方法添加上,目前想到的解决方法是把当前线程中的dao中的信息(dao的class、dao泛型)放入ThreadLocal,BaseSelectProvider执行方法时获取进行实例化,这就不用使用成员变量。

参考

mybatis官方文档:https://mybatis.org/mybatis-3/zh_CN/index.html