likes
comments
collection
share

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

作者站长头像
站长
· 阅读数 43

Mybatis Plus 空字符串插入问题引起的思考

在这篇文章中,结合笔者在工作中使用到 Mybatis Plus 的过程中遇到的空字符串丢失的问题,谈一谈对 Mybatis Plus 的一些看法。由于项目历史问题,使用到的 Mybatis Plus 的版本如下:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0-beta</version>
</dependency>

一、问题背景

我们存在如下数据库表:

create table ad_person_send_rule
(
    id                  bigint unsigned auto_increment
        primary key,
    status              tinyint(3)                default 1                 not null comment '状态',
    operator_user       varchar(128) charset utf8 default ''                not null comment '更新人',
    account_type        varchar(10) charset utf8  default ''                not null comment '账户类型',
    account_kinds       varchar(10) charset utf8  default ''                not null comment '账户性质',
    include_account_ids text charset utf8                                   null comment '允许的账户id',
    exclude_account_ids text charset utf8                                   null comment '排除账户id',
    create_time         timestamp                 default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time         timestamp                 default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)

对于 text 类型的 exclude_account_ids 字段,是不能设置默认值的,这是由于 MySQL 5.7 的严格模式导致的,什么都不写的话只能为空。对于业务来说,exclude_account_ids 空字符串是有意义的,所以在某些情况下,我们需要直接插入空字符串。

二、Mybatis Plus 执行插入过程丢失空字符串

构建对应的数据库实体类:

public class AdPersonSendRule implements Serializable {
    @TableField(exist = false)
    private static final long serialVersionUID = -3646240230891328158L;
    @TableId(type = IdType.AUTO)
    private Long id;
    private Integer status;
    private String operatorUser;
    private String accountType;
    private String accountKinds;
    private String includeAccountIds;
    private String excludeAccountIds;
    private Date createTime;
    private Date updateTime;
}

执行插入操作:

public void createPersonAuditRuleTest() {
    PersonAuditRuleRequest personAuditRuleRequest = new PersonAuditRuleRequest();
    personAuditRuleRequest.setIncludeAccountIDs("129,240,9123,100");
    // 设置为空字符串
    personAuditRuleRequest.setExcludeAccountIDs("");
    personAuditRuleRequest.setAccountKinds(new HashSet<>());
    personAuditRuleRequest.setAccountTypes(new HashSet<>());
    Result robotAuditRule = personAuditController.createPersonAuditRule(personAuditRuleRequest);
    log.info("插入结果结果为:{}", JSONUtil.toJsonStr(robotAuditRule));
}

其中我们的 excludeAccountIds 字段的值为空字符串,数据库的插入结果为:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

若把 excludeAccountIds 字段上标注 @TableField 注解后,数据库中插入结果为:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

可见,没有带 @TableField 注解时,插入空字符串对应的值为 null,而带了 @TableField 注解时,插入空字符串对应的值为空字符串,这才是我想要的结果,但是,为什么会造成这样的情况呢,我们要到看 Mybatis Plus 底层实际插入的 SQL 是什么,接下来我们从源码的角度出发去分析。

三、源码剖析 Mybatis Plus 中 SQL 解析与执行过程

我们应该如何去排查这个问题呢?众所周知,Mybatis Plus 框架其实是对 Mybatis 框架的进一步封装,提供一些更加便捷的方式,所以我们可以从 Mybatis 框架的执行流程去排查分析流程。

在 Mybatis 中,一条 SQL 的执行的流程如下图所示:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

3.1 SQL 执行过程分析

所以,我们可以在 PrepareStatementHandler 声明语句处理器中打下断点,去排查原因。对于我们的 insert 方法来说,在 PrepareStatementHandler 中实际上调用的是 update 方法(其实 insert、update、delete 的 SQL 语句的执行都会调用该类的 update 方法)

@Override
public int update(Statement statement) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    int rows = ps.getUpdateCount();
    Object parameterObject = boundSql.getParameterObject();
    KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
    keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
    return rows;
}

对于没有带 @TableField 情况,我们可以查看 boundSql这个成员变量:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

  • 可见,插入的 SQL 语句为INSERT INTO ad_person_send_rule(status,operator_user,include_account_ids) VALUES ( ?,?,? )
  • 明显不存在我们想要的 exclude_account_ids 字段;

对于带 @TableField 情况,我们可以继续查看 boundSql这个成员变量:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

  • 可见,插入的 SQL 语句为 INSERT INTO ad_person_send_rule (status,operator_user,include_account_ids,exclude_account_ids )VALUES ( ?,?,?,? )
  • 明显不存在我们想要的 exclude_account_ids 字段,对应的参数为空字符串。

3.2 SQL 解析过程分析

为什么只因为一个注解,Mybatis Plus 解析后生成 SQL 语句会不同呢?这就要从 Mybatis Plus 的解析 SQL 语句开始找答案了。

在 Mybatis Plus 中,SQL 的解析其实跟 Mybatis 大同小异,入口基本都是 XmlMapperBuilder 类。对于注解类型的注入,是在 MybatisMapperAnnotationBuilderparse 方法进行解析的。但是对于 BaseMapper 接口里面的方法,Mybatis Plus 会优先进行解析:

@Override
public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
        loadXmlResource();
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        Method[] methods = type.getMethods();
        //d注入 CURD 动态 SQL (应该在注解之前注入)
        if (BaseMapper.class.isAssignableFrom(type)) {
            GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);
        }
        for (Method method : methods) {
            try {
                // issue #237
                if (!method.isBridge()) {
                    parseStatement(method);
                }
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

核心解析方法就是 AbstractSqlInjector 的 inspectInject 方法:

@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
    String className = mapperClass.toString();
    Set<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
    if (!mapperRegistryCache.contains(className)) {
        List<AbstractMethod> methodList = this.getMethodList();
        if (CollectionUtils.isEmpty(methodList)) {
            throw new MybatisPlusException("No effective injection method was found.");
        }
        // 循环注入BaseMapper方法
        methodList.forEach(m -> m.inject(builderAssistant, mapperClass));
        mapperRegistryCache.add(className);
    }
}
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
    configuration = builderAssistant.getConfiguration();
    this.builderAssistant = builderAssistant;
    languageDriver = configuration.getDefaultScriptingLanguageInstance();
    Class<?> modelClass = extractModelClass(mapperClass);
    if (null != modelClass) {
        // 注入自定义方法
        TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
        injectMappedStatement(mapperClass, modelClass, tableInfo);
    }
}
  • 对于 insert 方法,AbstractMethod 的实现类是 Insert,所以调用 injectMappedStatement 方法时,实际上是调用 Insert 类的 injectMappedStatement 方法;

对于 insert 方法的解析:

public class Insert extends AbstractMethod {
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        //...
        StringBuilder fieldBuilder = new StringBuilder();
        StringBuilder placeholderBuilder = new StringBuilder();
        SqlMethod sqlMethod = SqlMethod.INSERT_ONE;
        fieldBuilder.append("\n<trim prefix="(" suffix=")" suffixOverrides=",">");
        placeholderBuilder.append("\n<trim prefix="(" suffix=")" suffixOverrides=",">");
        // 是否 IF 标签判断
        List<TableFieldInfo> fieldList = tableInfo.getFieldList();
        for (TableFieldInfo fieldInfo : fieldList) {
            if (FieldFill.INSERT == fieldInfo.getFieldFill()
                || FieldFill.INSERT_UPDATE == fieldInfo.getFieldFill()) {
                fieldBuilder.append(fieldInfo.getColumn()).append(",");
                placeholderBuilder.append("#{").append(fieldInfo.getEl()).append("},");
            } else {
                fieldBuilder.append(convertIfTagIgnored(fieldInfo, false));
                fieldBuilder.append(fieldInfo.getColumn()).append(",");
                fieldBuilder.append(convertIfTagIgnored(fieldInfo, true));
                placeholderBuilder.append(convertIfTagIgnored(fieldInfo, false));
                placeholderBuilder.append("#{").append(fieldInfo.getEl()).append("},");
                placeholderBuilder.append(convertIfTagIgnored(fieldInfo, true));
            }
        }
        fieldBuilder.append("\n</trim>");
        placeholderBuilder.append("\n</trim>");
        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), fieldBuilder.toString(), placeholderBuilder.toString());
        //构造sql语句
        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        return this.addInsertMappedStatement(mapperClass, modelClass, sqlMethod.getMethod(), sqlSource, keyGenerator, keyProperty, keyColumn);
    }
}
  • 最终构造 SQL 语句其实是 createSqlSource 方法,但是现在已经可以看出 SQL 是如何构造出来的了。

我们来看看没有 TableField 情况,构造出实际的 SQL 如下图:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

<script>INSERT INTO ad_person_send_rule 
  <trim prefix="(" suffix=")" suffixOverrides=",">
    <if test="status!=null ">status,</if>
    <if test="operatorUser!=null and operatorUser!=''">operator_user,</if>
    <if test="accountType!=null and accountType!=''">account_type,</if>
    <if test="accountKinds!=null and accountKinds!=''">account_kinds,</if>
    <if test="includeAccountIds!=null and includeAccountIds!=''">include_account_ids,</if>
    <if test="excludeAccountIds!=null and excludeAccountIds!=''">exclude_account_ids,</if>
    <if test="createTime!=null ">create_time,</if><if test="updateTime!=null ">update_time,</if>
  </trim> VALUES 
  <trim prefix="(" suffix=")" suffixOverrides=","><if test="status!=null ">#{status},</if>
    <if test="operatorUser!=null and operatorUser!=''">#{operatorUser},</if>
    <if test="accountType!=null and accountType!=''">#{accountType},</if>
    <if test="accountKinds!=null and accountKinds!=''">#{accountKinds},</if>
    <if test="includeAccountIds!=null and includeAccountIds!=''">#{includeAccountIds},</if>
    <if test="excludeAccountIds!=null and excludeAccountIds!=''">#{excludeAccountIds},</if>
    <if test="createTime!=null ">#{createTime},</if><if test="updateTime!=null ">#{updateTime},</if>
  </trim>
</script>
  • 可以看出在 excludeAccountIds 字段中,它的判断条件只是 excludeAccountIds!=null and excludeAccountIds!='',所以对于空字符串的情况,它是可以不插入的。

对于带了 TableField 注解的情况,构造出实际的 SQL 如下图:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

<script>INSERT INTO ad_person_send_rule 
  <trim prefix="(" suffix=")" suffixOverrides=",">
    <if test="status!=null ">status,</if>
    <if test="operatorUser!=null and operatorUser!=''">operator_user,</if>
    <if test="accountType!=null and accountType!=''">account_type,</if>
    <if test="accountKinds!=null and accountKinds!=''">account_kinds,</if>
    <if test="includeAccountIds!=null and includeAccountIds!=''">include_account_ids,</if>
    <if test="excludeAccountIds!=null">exclude_account_ids,</if>
    <if test="createTime!=null ">create_time,</if>
    <if test="updateTime!=null ">update_time,</if>
  </trim> VALUES 
  <trim prefix="(" suffix=")" suffixOverrides=",">
    <if test="status!=null ">#{status},</if>
    <if test="operatorUser!=null and operatorUser!=''">#{operatorUser},</if>
    <if test="accountType!=null and accountType!=''">#{accountType},</if>
    <if test="accountKinds!=null and accountKinds!=''">#{accountKinds},</if>
    <if test="includeAccountIds!=null and includeAccountIds!=''">#{includeAccountIds},</if>
    <if test="excludeAccountIds!=null">#{excludeAccountIds},</if><if test="createTime!=null ">#{createTime},</if>
    <if test="updateTime!=null ">#{updateTime},</if>
  </trim>
</script>
  • 可以看出在 excludeAccountIds 字段中,它的判断条件只是 excludeAccountIds!=null,所以对于空字符串的情况,它是可以插入的。

所以我们就可以得出,其实是 test 中判断条件的不同,导致生成 SQL 的不同,而 test 中的判断条件,取决于 TableFieldInfo 类中的 FieldStrategy 字段:

public enum FieldStrategy {
    IGNORED(0, "忽略判断"),
    NOT_NULL(1, "非 NULL 判断"),
    NOT_EMPTY(2, "非空判断");
}

没有 TableField 注解情况,它的类型是 NOT_EMPTY:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

我们看看没有有 TableField参数 的 TableFieldInfo 的构造函数:

public TableFieldInfo(boolean underCamel, GlobalConfig.DbConfig dbConfig, TableInfo tableInfo, Field field) {
    if (dbConfig.isColumnUnderline()) {
        /* 开启字段下划线申明 */
        this.setColumn(dbConfig, StringUtils.camelToUnderline(field.getName()));
        /* 未开启下划线转驼峰模式 AS 转换 */
        if (!underCamel) {
            this.related = true;
        }
    } else {
        this.setColumn(dbConfig, field.getName());
    }
    this.property = field.getName();
    this.el = field.getName();
    //全局配置,默认是NOT_EMPTY
    this.fieldStrategy = dbConfig.getFieldStrategy();
    this.propertyType = field.getType();
    this.setCondition(dbConfig);
    tableInfo.setLogicDelete(this.initLogicDelete(dbConfig, field));
}
  • 默认使用 dbConfig 的全局配置,默认是 NOT_EMPTY;

有 TableField 注解情况,它的类型是 NOT_NULL:

Mybatis Plus 空字符串插入问题引起的思考Mybatis Plus 空字符串插入问题引起的思考 在这篇文章中,

我们看看有 TableField 参数 的 TableFieldInfo 的构造函数:

//这是有
public TableFieldInfo(boolean underCamel, GlobalConfig.DbConfig dbConfig, TableInfo tableInfo,
                          String column, String el, Field field, TableField tableField) {
    //...
    // 默认使用TableField中的NOT_NULL
    if (dbConfig.getFieldStrategy() != tableField.strategy()) {
        this.fieldStrategy = tableField.strategy();
    } else {
        this.fieldStrategy = dbConfig.getFieldStrategy();
    }
    //...
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface TableField {
    String value() default "";
    String el() default "";
    boolean exist() default true;
    String condition() default "%s=#{%s}";
    String update() default "";
    FieldStrategy strategy() default FieldStrategy.NOT_NULL;
    FieldFill fill() default FieldFill.DEFAULT;
}
  • 默认使用 @TableField 的默认配置,默认是 NOT_EMPTY;

四、关于 Mybatis Plus 的看法

在 Mybatis Plus 框架中,就一个简单的 insert 方法,因为 TableField 中 FieldStrategy 默认配置的问题,导致 insert 有不同的 SQL 实现方式。况且 TableField 注解中的属性,我们不会经常去使用,最常用到的只是 value 字段,做一个 column 名称的映射罢了。在我经常使用到的 MybatisX 生成插件中是不会生成 TableField 字段的,默认使用 DO 类的类名去做数据库字段的映射。

我们回到刚开始问题,如果我不配置 TableField 字段,那么对于空字符串来说,是不是就插入不到表中了呢?这是不是不太合理呢?毕竟在这里,空字符串是符合业务逻辑的。虽然可以通过配置 TableField 注解来避免这种情况,又或者是升级 Mybatis Plus 版本去解决这个问题,但这个排查问题过程着实让人烦恼,更别说没有接触过 Mybatis 源码的同学了。

在某种层面中,Mybatis Plus 可以提高我们开发的效率,提供了很多便捷的数据库操作的方法。但是我始终认为:「SQL 的执行是关乎数据的生成与变更的,这么重要的语句应该暴露在我们开发者的眼中,而不是让框架的底层把这些语句给屏蔽掉,所以我更建议使用配置 XML 文件的方式去编写 SQL,这样 SQL 清晰且可控。」

转载自:https://juejin.cn/post/7420827144110997544
评论
请登录