通过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同时存在的情况