Mybatis-Plus 自动属性填充与自定义Insert into语句顺序

前言:系统中使用了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 来完成,具体流程如下:

  1. Mybatis 在执行 SQL 语句之前,会根据 Mapper 接口定义的方法和传入的参数生成 MappedStatement 对象。在 MappedStatement 中包含了 SQL 语句、参数映射信息等相关的元数据。
  2. MappedStatement 负责生成 BoundSql 对象。在生成 BoundSql 对象时,Mybatis 会首先根据 SQL 语句和参数信息生成一个 StaticSqlSource 对象,然后再通过它生成一个 DynamicSqlSource 对象。DynamicSqlSource 会根据传入的参数信息和 Mapper 接口定义的 SQL 语句,动态生成最终的 SQL 语句和参数值。这个过程中,ParameterMapping 负责将 Java 对象中的属性值和 SQL 语句中的占位符做映射关联,SqlSource 负责根据参数信息和 SQL 语句生成 BoundSql 对象。
  3. 生成 BoundSql 对象后,Mybatis 会通过 ParameterHandler 将 BoundSql 对象中的 SQL 语句和参数值设置到 JDBC 预处理语句中。默认的 ParameterHandler 实现类是 DefaultParameterHandler,它会通过反射获取 PreparedStatement 对象,并调用 setXxx() 方法将参数值设置到预处理语句中。在设置参数值的过程中,DefaultParameterHandler 会根据 ParameterMapping 中的信息获得 Java 对象中对应属性的值,并将其赋值给 BoundSql 对象中对应的参数占位符。
  4. 综上所述,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会动态的替换掉改占位符。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值