前言:系统中使用了Mybatis-Plus 自动属性填充为实体统一进行属性的填值,在Mapper的xml 文件中 insert into 语句 使用
<if test="id != null">id,</if>
进行判断会发现该属性是空的,明明已经为改字段进行了属性的自动填充,为什么Mybatis- 在拼接sql 语句时依然认为 改属性是空的呢;
1 问题重现:
1.1 在实体中使用了属性填充属性:
@TableField(fill = FieldFill.INSERT)
private String testFiled;
1.2 在拦截器里进行了属性填充:
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("testFiled", "test", metaObject);
}
1.3 mapper xml :
insert into ${prefix}knowledge_authority
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="testFiled != null">test_filed,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="testFiled != null">#{testFiled },</if>
</trim>
在对实体id 设置完成之后,进行数据的插入,发现插入的数据中只有id 没有testFiled 属性;
2 推断问题产生的原因:
原因1:属性填充正常,但是在xml sql 语句中,在某些情况下 <if>
判断有问题;
原因2:自定义填充属性有问题,导致想要填充的属性没有被填充值,导致进行 <if>
判断有问题;
愿意3:属性填充和 <if>
标签判断都没有问题,但是sql 拼接的时机 在属性填充之前进行;
<if>
标签只进行简单的空判断,出问题的可能性不大,从原因2 入手:
使用自定义属性填充时,会调用MybatisParameterHandler 类中的process()方法完成属性填充的调用;
private void process(Object parameter) {
if (parameter != null) {
TableInfo tableInfo = null;
Object entity = parameter;
if (parameter instanceof Map) {
Map<?, ?> map = (Map)parameter;
if (map.containsKey("et")) {
Object et = map.get("et");
if (et != null) {
entity = et;
tableInfo = TableInfoHelper.getTableInfo(et.getClass());
}
}
} else {
tableInfo = TableInfoHelper.getTableInfo(parameter.getClass());
}
if (tableInfo != null) {
MetaObject metaObject = this.configuration.newMetaObject(entity);
if (SqlCommandType.INSERT == this.sqlCommandType) {
// 插入时 id 的填充
this.populateKeys(tableInfo, metaObject, entity);
// 这里会在insert 时 调用我们自己定义的拦截器进行属性的自动填充
this.insertFill(metaObject, tableInfo);
} else {
// 这里会在update 时 调用我们自己定义的拦截器进行属性的自动填充
this.updateFill(metaObject, tableInfo);
}
}
}
}
通过debug 我们发现,在插入数据时确实调用了process 方法,并对实体完成了属性的填充,属性填充是正常的;所以会不会是原因3 ,属性填充的时机和sql 拼接的时机不同造成的。
如果 先进行了sql的拼接,此时进行 <if>
判断时 发现改属性为空,必然会跳过了该属性的拼接,即使后面自动填充为属性填充了数据,但是由于sql已经完成了拼接,最终执行的sql 也是没有该属性的;
基于此猜想,我们将xml 中 判断标签去掉,只保留占位符:
insert into ${prefix}knowledge_authority
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
test_filed,
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
#{testFiled },
</trim>
此时在次进行插入,发现插入成功,并且testFiled 属性也是有值的;
3 从Mybatis-Plus 代码层面查看sql 语句的拼接:
3.1 先看下sql 拼接的流程:
MybatisParameterHandler 是 Mybatis 中用于处理数据库操作参数的接口,它的实现类 DefaultParameterHandler 负责将 Java 对象转换为 JDBC 预处理语句需要的参数值,以及将参数值设置到预处理语句中。其中,BoundSql 对象就是用于封装 SQL 语句和对应的参数值的。
BoundSql 对象的赋值过程主要由 SqlSource 和 ParameterMapping 来完成,具体流程如下:
- Mybatis 在执行 SQL 语句之前,会根据 Mapper 接口定义的方法和传入的参数生成 MappedStatement 对象。在 MappedStatement 中包含了 SQL 语句、参数映射信息等相关的元数据。
- MappedStatement 负责生成 BoundSql 对象。在生成 BoundSql 对象时,Mybatis 会首先根据 SQL 语句和参数信息生成一个 StaticSqlSource 对象,然后再通过它生成一个 DynamicSqlSource 对象。DynamicSqlSource 会根据传入的参数信息和 Mapper 接口定义的 SQL 语句,动态生成最终的 SQL 语句和参数值。这个过程中,ParameterMapping 负责将 Java 对象中的属性值和 SQL 语句中的占位符做映射关联,SqlSource 负责根据参数信息和 SQL 语句生成 BoundSql 对象。
- 生成 BoundSql 对象后,Mybatis 会通过 ParameterHandler 将 BoundSql 对象中的 SQL 语句和参数值设置到 JDBC 预处理语句中。默认的 ParameterHandler 实现类是 DefaultParameterHandler,它会通过反射获取 PreparedStatement 对象,并调用 setXxx() 方法将参数值设置到预处理语句中。在设置参数值的过程中,DefaultParameterHandler 会根据 ParameterMapping 中的信息获得 Java 对象中对应属性的值,并将其赋值给 BoundSql 对象中对应的参数占位符。
- 综上所述,BoundSql 对象的赋值过程主要由 SqlSource 和 ParameterMapping 来完成。它们会根据传入的参数信息和 Mapper 接口定义的 SQL 语句,动态生成最终的 SQL 语句和参数值,并将它们设置到 BoundSql 对象中。
3.2 MybatisParameterHandler 中的 BoundSql boundSql:
public class MybatisParameterHandler implements ParameterHandler {
private final TypeHandlerRegistry typeHandlerRegistry;
private final MappedStatement mappedStatement;
private final Object parameterObject;
private final BoundSql boundSql;
private final Configuration configuration;
private final SqlCommandType sqlCommandType;
public MybatisParameterHandler(MappedStatement mappedStatement, Object parameter, BoundSql boundSql) {
this.typeHandlerRegistry = mappedStatement.getConfiguration().getTypeHandlerRegistry();
this.mappedStatement = mappedStatement;
// 拼接好的sql
this.boundSql = boundSql;
this.configuration = mappedStatement.getConfiguration();
this.sqlCommandType = mappedStatement.getSqlCommandType();
// 主键id 和 属性的自动填充
this.parameterObject = this.processParameter(parameter);
}
}
可以看到在创建MybatisParameterHandler 对象时,boundSql 已经完成了sql 的解析和拼接,然后在this.processParameter(parameter) 方法完成了主键id 和 属性的自动填充,从构造方法可以看到,boundSql 的拼接是先于processParameter(parameter) 属性填充的方法的,这就解释了为什么我们明明已经为改属性进行了填充,为什么 最终自定义的insert into 语句 标签判断是空的,本质就是因为两者的顺序问题;
3.3 sql 语句的拼接:
进入DynamicSqlSource 类getBoundSql 方法:
public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
public BoundSql getBoundSql(Object parameterObject) {
// 参数解析
DynamicContext context = new DynamicContext(this.configuration, parameterObject);
// sql 拼接
this.rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(this.configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 占位符拼接
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
// sql 拼接
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
}
this.rootSqlNode.apply(context):
public class MixedSqlNode implements SqlNode {
private final List<SqlNode> contents;
public MixedSqlNode(List<SqlNode> contents) {
this.contents = contents;
}
public boolean apply(DynamicContext context) {
// 这里会判断xml 中的所有属性标签,只有判断为true ,才进行属性的拼接
this.contents.forEach((node) -> {
node.apply(context);
});
return true;
}
}
可以看到这里会根据标签不同调用不同的实现完成判断:
并且会逐个进行属性的判断,只有为true 才进行属性拼接:
以IfSqlNode 为例,可以看出只有当属性不为空时,才返回true 否则返回false,只有在返回true 时后续才会对改属性进行拼接
4 总结:
Mybatis-Plus 自定义的sql 语句其BoundSql的解析和拼接是在属性填充之前进行的,所以如果在自定义sql 语句中使用了<if>
标签进行属性的非空判断,就不会拼接改属性,此时需要在自定义的sql 中去除<<if>
的非空判断直接使用#{testFiled },这样最终在进数据插入时,Mybatis会动态的替换掉改占位符。