Mybatis 插件兼容动态SQL
2021-10-24 本文已影响0人
晴天哥_王志
系列
开篇
-
在mybatis的语法当中支持一种动态SQL的语法,比较常见的是<if>条件判断,这类语句会结合实际传入的参数拼接完整的待执行的SQL 语句。
-
在某种场景下我们会需要在 mybatis的拦截器中动态修改入参并期望生成新的SQL语句。
-
一般场景下上述的情况应该在 Executor 的拦截插件中,但是事实上可以在ParameterHandler或StatementHandler 插件当中进行调整。
-
建议通过实际实践可以发现真正的逻辑,如果能够能完整的调试会更加印象深刻。
例子
<insert id="addOneUser" parameterType="com.tuacy.mybatis.interceptor.entity.vo.UserInfoVo">
INSERT INTO user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="null != pkId">
pkid,
</if>
<if test="null != userName">
`name`,
</if>
<if test="null != password">
password,
</if>
<if test="null != phone">
phone
</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="null != pkId">
#{pkId},
</if>
<if test="null != userName">
#{userName},
</if>
<if test="null != password">
#{password},
</if>
<if test="null != phone">
#{phone},
</if>
</trim>
</insert>
- 上述SQL当中我们通过修改入参对象的值来动态修改生成的INSERT语句,如修改 phone字段为 null,那么整个INSERT语句就不包含phone字段。
ParameterHandler拦截
image.png- mybatis真正执行的SQL对象是 BoundSql 对象,而 BoundSql 语句是通过MappedStatement来返回的。
- MappedStatement.getBoundSql是通过sqlSource.getBoundSql 语句来生成 BoundSql对象。
- 通过动态修改 SqlSource 对象 或 修改MappedStatement来达到目的
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)
})
public class ParamInterceptor implements Interceptor {
private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取拦截器拦截的设置参数对象DefaultParameterHandler
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 通过mybatis的反射来获取对应的值
MetaObject metaParameterHandler = MetaObject.forObject(parameterHandler, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
MappedStatement mappedStatement = (MappedStatement) metaParameterHandler.getValue("mappedStatement");
Object parameterObject = metaParameterHandler.getValue("parameterObject");
// id字段对应执行的SQL的方法的全路径,包含类名和方法名
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1);
// 动态加载类并获取类中的方法
final Method[] methods = Class.forName(className).getMethods();
// 遍历类的所有方法并找到此次调用的方法
for (Method method : methods) {
if (method.getName().equalsIgnoreCase(methodName)
&& method.isAnnotationPresent(ParamAnnotation.class)) {
// 获取方法上的注解以及注解对应的参数
ParamAnnotation paramAnnotation = method.getAnnotation(ParamAnnotation.class);
String srcKey = paramAnnotation.srcKey()[0];
String destKey = paramAnnotation.destKey()[0];
// 反射获取参数对象
MetaObject param = MetaObject.forObject(parameterObject, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
Object srcValue = param.getValue(srcKey);
// 动态加工指定参数
String destValue = String.valueOf(srcValue) + "fix";
// 将修改后的动态参数添加到请求参数当中
param.setValue(destKey, destValue);
param.setValue(destKey, null);
break;
}
}
// 修改入参后动态生成的BoundSql对象
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
String newSql = boundSql.getSql();
// 生成新的BoundSql
BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), newSql,
boundSql.getParameterMappings(), boundSql.getParameterObject());
// 1、通过生成新的MappedStatement对象
MappedStatement newMappedStatement = newMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}
metaParameterHandler.setValue("mappedStatement", newMappedStatement);
// 2、通过生成新的SqlSource对象
MetaObject metaMappedStatement = MetaObject.forObject(mappedStatement, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, REFLECTOR_FACTORY);
metaMappedStatement.setValue("sqlSource", new BoundSqlSqlSource(newBoundSql));
// 回写parameterObject对象
metaParameterHandler.setValue("parameterObject", parameterObject);
return invocation.proceed();
}
class BoundSqlSqlSource implements SqlSource {
private BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
private MappedStatement newMappedStatement (MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new
MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
- 1、通过BoundSql boundSql = mappedStatement.getBoundSql(parameterObject)来获取新的BoundSql对象,进而获取文本的SQL语句。
- 2、通过BoundSql的构造函数构建新的BoundSql 对象。
- 3、BoundSqlSqlSource继承SqlSource来生成 SqlSource 对象。
- 4、通过newMappedStatement生成新的MappedStatement对象。
- 步骤3和步骤4只要选择其中一个就可以了,需要实际实践一下。