ty.mybatis 动态生成SQL原理,源码解析:ty.mybatis接口使用不慎导致的全表删除

问题

今天遇到了一个问题,某个版本之后,出现了几次某张数据表被清空的情况,第一次还以为是谁手动删除的数据,但是出现第二第三次,就怀疑到代码头上了,找了一下是有人使用第三方tk.mybatis的方法不慎导致的bug,接下来讲一下分析过程和结论

Tkmybatis是基于Mybatis框架开发的一个工具,通过调用它提供的方法实现对单表的数据操作,不需要写任何sql语句,这极大地提高了项目开发效率

定位问题

全局搜了操作被清空表的地方,mapper.xml里没有delete语句,那就可能是调用了第三方方法导致不规范导致的,然后定位到下图的代码,delete()
在这里插入图片描述

发现是tk.mybatis包封装的方法,这就应证了猜想,入参有问题,具体源码分析放下一章,总的来说,就是传入参数是null,导致拼接的动态SQL没有where和之后的限制条件,导致全表删除

在这里插入图片描述

入参就一个set了主键id的对象

来找一下调用的地方,发现在controller层的一个finally块中,每次必被调用

在这里插入图片描述

看一下接口信息,接口传入了该参数,
而在代码中,参数必被置为null,然后被赋予新值

经过了解,这个很长的方法代码是贴过来用的,中间产生了许多无效的操作和数据库操作,所以要最后执行一下清除操作

为什么8月前后发生了几次清表操作呢,因为之前这个删除操作不在finally中,被修改后,try块中的代码只要发生错误,就会走到finally,此时临时的参数值还没有生成,导致直接执行

delete from ******

源码分析

项目中基本所有mapper都实现了Mapper接口,下面是继承路径

	Mapper接口								--tk.mybatis.mapper.common
		BaseMapper接口						--tk.mybatis.mapper.common
			BaseDeleteMapper接口				--tk.mybatis.mapper.common.base
				DeleteMapper接口				--tk.mybatis.mapper.common.base.delete
					BaseDeleteProvider类		--tk.mybatis.mapper.provider.base

在DeleteMapper接口中,@DeleteProvider注解的type属性指定了BaseDeleteProvider类

	/**
     * 根据实体属性作为条件进行删除,查询条件使用等号
     *
     * @param record
     * @return
     */
    @DeleteProvider(type = BaseDeleteProvider.class, method = "dynamicSQL")
    int delete(T record);

BaseDeleteProvider类中,就是delete()方法本法

/**
     * 通过条件删除
     *
     * @param ms
     * @return
     */
    public String delete(MappedStatement ms) {
    
    
        //获取实体类
        Class<?> entityClass = getEntityClass(ms);
        StringBuilder sql = new StringBuilder();
        //如果设置了安全删除,就不允许执行不带查询条件的 delete 方法,默认false不开启
        if (getConfig().isSafeDelete()) {
    
    
            sql.append(SqlHelper.notAllNullParameterCheck("_parameter", EntityHelper.getColumns(entityClass)));
        }
        // 如果是逻辑删除,则修改为更新表,修改逻辑删除字段的值,实际没有,所以false
        if (SqlHelper.hasLogicDeleteColumn(entityClass)) {
    
    
            sql.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
            sql.append("<set>");
            sql.append(SqlHelper.logicDeleteColumnEqualsValue(entityClass, true));
            sql.append("</set>");
            //修改为UPDATE
            MetaObjectUtil.forObject(ms).setValue("sqlCommandType", SqlCommandType.UPDATE);
        } else {
    
    
            //执行的是这一行,sql = "DELETE FROM 表名 "
            sql.append(SqlHelper.deleteFromTable(entityClass, tableName(entityClass)));
        }
        //添加条件,isNotEmpty中变量默认为不判断
        sql.append(SqlHelper.whereAllIfColumns(entityClass, isNotEmpty()));
        return sql.toString();
    }

deleteFromTable进行了表名解析

/**
     * delete tableName - 动态表名
     *
     * @param entityClass
     * @param defaultTableName
     * @return
     */
    public static String deleteFromTable(Class<?> entityClass, String defaultTableName) {
    
    
        StringBuilder sql = new StringBuilder();
        sql.append("DELETE FROM ");
        sql.append(getDynamicTableName(entityClass, defaultTableName));
        sql.append(" ");
        return sql.toString();
    }

whereAllIfColumns中进行了参数拼接

/**
     * where所有列的条件,会判断是否!=null
     *
     * @param entityClass
     * @param empty
     * @param useVersion
     * @return
     */
    public static String whereAllIfColumns(Class<?> entityClass, boolean empty, boolean useVersion) {
    
    
        StringBuilder sql = new StringBuilder();
        boolean hasLogicDelete = false;
		//添加的只是mybatis的标签
        sql.append("<where>");
        //获取全部列
        Set<EntityColumn> columnSet = EntityHelper.getColumns(entityClass);
        EntityColumn logicDeleteColumn = SqlHelper.getLogicDeleteColumn(entityClass);
        //当某个列有主键策略时,不需要考虑他的属性是否为空,因为如果为空,一定会根据主键策略给他生成一个值
        for (EntityColumn column : columnSet) {
    
    
            if (!useVersion || !column.getEntityField().isAnnotationPresent(Version.class)) {
    
    
                // 逻辑删除,后面拼接逻辑删除字段的未删除条件
                if (logicDeleteColumn != null && logicDeleteColumn == column) {
    
    
                    hasLogicDelete = true;
                    continue;
                }
                sql.append(getIfNotNull(column, " AND " + column.getColumnEqualsHolder(), empty));
            }
        }
        //加乐观锁
        if (useVersion) {
    
    
            sql.append(whereVersion(entityClass));
        }
        if (hasLogicDelete) {
    
    
            sql.append(whereLogicDelete(entityClass, false));
        }

        sql.append("</where>");
        return sql.toString();
    }

在项目启动时生成SQL模板,

delete()结果如下(举例):

DELETE FROM db_test.t_test_table 
<where>
	<if test="id != null"> 
		AND id = #{id}
	</if>
	<if test="col != null"> 
	AND col = #{col}
	</if>
</where>

deleteByPrimaryKey()结果如下:

DELETE FROM db_test.t_test_table 
<where> 
	AND id = #{
    
    id}
</where>

当使用的时候,就像我们拿自己写在mapper.xml中的SQL一样

至于tk mybatis怎么与mybatis整合,参数怎么拼进动态SQL,就另说了

结论

从源码和运行结果可知,delete()在参数全null的情况下回删除全表

代码中不止有delete(),还有deleteByPrimaryKey()、deleteByCondition()、updateByPrimaryKeySelective等方法,update也有把未设置值的字段更新为null的先例

建议

1、使用第三方函数时,建议看源码实现

2、如果不想参考第一点,自己写SQL比较好

3、在yml配置文件中设置safeDelete,见文档https://github.com/abel533/Mapper/wiki/3.config
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43859729/article/details/108012996
ty
今日推荐