通过Mybatis插件修改SQL

前言:在PostgresSQL数据库中,比MySQL多一个Schema的功能,相当于是数据库下面又分了一层,一个库里面可以有多个schema,不同schema下面可以由名字相同的表。如果需要全局修改schema就可以使用Mybatis插件的形式来实现同一套SQL去查询不同的表

由于没有安装PostgresSQL,本文仅展示使用插件修改SQL

前置:


User表、DTO、mapper对应的xml

一张测试表:

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(20) NOT NULL COMMENT '名字',
  PRIMARY KEY (`id`)
)

对应DTO:

@Data
public class User {
    private Integer id;
    private String name;
}

UserMapper:

@Mapper
public interface UserMapper {
    public List<User> selectAll();

    public User selectById(@Param("id") Integer id);
}

mapper对应的xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="fun.psgame.springbootlearn.mapper.UserMapper2">
    <select id="selectAll" resultType="User">
        <!-- 这里本来应该使用user,但是作为演示使用${table},类似${schema}.user的用法 -->
        select id, name from ${table}
    </select>

    <select id="selectById" resultType="User">
        select id, name from ${table} where id = #{id}
    </select>
</mapper>

可以看到mapper.xml的sql里并没有使用表名,如果是在postgresSQL里可以不写死schema的名字,都在mybatis的插件中进行修改

首先在application.yml中定义一下数据库连接和mybatis的配置文件:


spring配置文件

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/springboot_learn?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: sql/*.xml
  config-location: classpath:config/mybatis-config.xml

logging:
  level:
    fun.psgame.springbootlearn: DEBUG

然后是Mybatis配置文件:


mybatis配置文件

<?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>
    <settings>
        <!-- 插件会导致主键回写失效,暂时不清楚原因 -->
        <setting name="useGeneratedKeys" value="true"/>
    </settings>
    <typeAliases>
        <!-- 配置别名 -->
        <package name="fun.psgame.springbootlearn.dto"/>
    </typeAliases>

    <plugins>
        <!-- 查询插件1 -->
        <plugin interceptor="fun.psgame.springbootlearn.common.mybatis.QueryParamPlugin">
            <!-- 测试用属性 -->
            <property name="someProperty" value="100"/>
        </plugin>
        <!-- 查询插件2 -->
        <plugin interceptor="fun.psgame.springbootlearn.common.mybatis.Query2ParamPlugin">
        </plugin>
        <!-- 插入、更新插件 -->
        <plugin interceptor="fun.psgame.springbootlearn.common.mybatis.UpdateParamPlugin"/>
    </plugins>
</configuration>

创建一个插件基类:


public class MybatisPluginBase {
    // 这是指定拦截位置的注解@Signature中的args的序号
    public static final int PARAMETER_INDEX_MAPPED_STATEMENT = 0;
    public static final int PARAMETER_INDEX_PARAMETER = 1;
    public static final int PARAMETER_INDEX_ROW_BOUNDS = 2;
    public static final int PARAMETER_INDEX_RESULT_HANDLER = 3;
    public static final int PARAMETER_INDEX_CACHE_KEY = 4;
    public static final int PARAMETER_INDEX_BOUND_SQL = 5;

    public Map<String, Object> createSessionParameterMap() {
        Map<String, Object> parameterMap = new HashMap<>();
        // 将table替换为user,如果是postgreSQL则将命名空间替换为session中的schema
        parameterMap.putIfAbsent("table", "user");

        return parameterMap;
    }

    /**
     * 创建参数替换的结果
     *
     * @param args 拦截方法的参数属性
     * @return 参数替换后的数据,主要是轻重的sqlMap属性
     */
    @SuppressWarnings({"rawtypes", "unchecked"})
    public SqlParameterResult createSqlParameterResult(Object[] args) {
        SqlParameterResult result = new SqlParameterResult();

        Map parameterMap = new HashMap<String, Object>();
        // 当执行的mapper接口方法有参数时
        if (args[PARAMETER_INDEX_PARAMETER] != null) {
            // 如果是多个值会存储在map中
            if (args[PARAMETER_INDEX_PARAMETER] instanceof Map) {
                parameterMap = (Map) args[PARAMETER_INDEX_PARAMETER];
            } else if (BeanUtils.isSimpleValueType(args[PARAMETER_INDEX_PARAMETER].getClass())) {
                //如果是简单类型就直接设置值
                parameterMap.putIfAbsent("arg0", args[PARAMETER_INDEX_PARAMETER]);
            } else {
                // 如果是DTO类型,反射获取字段和值的map
                Object dto = args[PARAMETER_INDEX_PARAMETER];
                // 获取属性
                PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(dto.getClass());
                for (int i = 0; i < propertyDescriptors.length; i++) {
                    PropertyDescriptor p = propertyDescriptors[i];
                    // spring的BeanUtils.getPropertyDescriptors会把class也获取到
                    if (p.getPropertyType().equals(Class.class)) continue;

                    Object value = null;
                    try {
                        // 读取实际值
                        value = p.getReadMethod().invoke(dto);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    parameterMap.putIfAbsent(p.getName(), value);
                }

                result.setHasDto(true);
                result.setDto(dto);
            }
        }

        Map sessionParameterMap = createSessionParameterMap();
        parameterMap.putAll(sessionParameterMap);

        result.setSqlMap(parameterMap);


        return result;
    }

    private String addQuotes(String s) {
        return '"' + s + '"';
    }

    @Getter
    @Setter
    protected class SqlParameterResult {
        // 将接口中传入的参数修改为map
        private Map sqlMap;
        // 如果有dto,把dto传出去进行主键回写
        private boolean hasDto = false;
        private Object dto;
    }
}

然后是具体的插件类:


// 指定拦截的类及其方法
@Intercepts({
        // 方法签名
        @Signature(
                // 所在的类
                type = Executor.class,
                // 方法名
                method = "query",
                // 方法的参数
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class QueryParamPlugin extends MybatisPluginBase implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取到拦截方法的参数
        Object[] args = invocation.getArgs();
        // 创建sql参数的map
        args[PARAMETER_INDEX_PARAMETER] = createSqlParameterMap(args);
        // 创建新的调用对象(参数被覆盖)
        Invocation overrideInvocation = new Invocation(invocation.getTarget(), invocation.getMethod(), args);
        // 运行新的调用对象
        return overrideInvocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 执行过程中会数次经过这里
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以获取mybatis配置文件中设置的属性
        System.out.println("properties = " + properties);
    }
}


不同的query方法

@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class }) })
public class Query2ParamPlugin extends MybatisPluginBase implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        args[PARAMETER_INDEX_PARAMETER] = createSqlParameterMap(args);
        Invocation overrideInvocation = new Invocation(invocation.getTarget(), invocation.getMethod(), args);
        return overrideInvocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

拦截更新、插入的方法


@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class UpdateParamPlugin extends MybatisPluginBase implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取拦截的方法的参数
        Object[] args = invocation.getArgs();
        // 根据方法的参数修改sql的参数
        SqlParameterResult sqlParameterMap = createSqlParameterResult(args);
        // 将参数替换成map(不特殊处理会造成主键回写失效)
        args[PARAMETER_INDEX_PARAMETER] = sqlParameterMap.getSqlMap();
        // 构建新的执行器
        Invocation overrideInvocation = new Invocation(invocation.getTarget(), invocation.getMethod(), args);
        // 执行(此时传出的参数中已经有主键)
        Object result = overrideInvocation.proceed();

        // 设置主键
        MappedStatement mappedStatement = (MappedStatement) overrideInvocation.getArgs()[PARAMETER_INDEX_MAPPED_STATEMENT];
        Map<String, Object> argMap = (Map<String, Object>) overrideInvocation.getArgs()[PARAMETER_INDEX_PARAMETER];
        if (sqlParameterMap.getHasDto() && mappedStatement.getConfiguration().isUseGeneratedKeys()) {
            String[] keyProperties = mappedStatement.getKeyProperties();
            for (String keyProperty : keyProperties) {
                BeanUtils.setProperty(sqlParameterMap.getDto(), keyProperty, argMap.get(keyProperty));
            }
        }

        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {}
}

还没有实验普通参数与DTO同时存在的情况

标签: SQL, Mybatis

添加新评论