通過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同時存在的情況