tk.mybatis 中 delete () 和 deleteByPrimaryKey () 的区别及源码解析

出现问题

在使用 tk.mybatis 的过程中,在拿到对象后,需要删除时,贪图方便直接调用了 delete () 删除方法,而当对象为空时,缺失判断导致删除了全表,在此对问题进行排查分析。

源码分析

删除接口 DeleteMapper

tk.mybatis 中删除接口如下所示:

package tk.mybatis.mapper.common.base.delete;

import org.apache.ibatis.annotations.DeleteProvider;
import tk.mybatis.mapper.annotation.RegisterMapper;
import tk.mybatis.mapper.provider.base.BaseDeleteProvider;

/**
 * 通用Mapper接口,删除
 *
 * @param <T> 不能为空
 * @author liuzh
 */
@RegisterMapper
public interface DeleteMapper<T> {

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

}

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

删除实现 DeleteProvider

package tk.mybatis.mapper.provider.base;

import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import tk.mybatis.mapper.mapperhelper.EntityHelper;
import tk.mybatis.mapper.mapperhelper.MapperHelper;
import tk.mybatis.mapper.mapperhelper.MapperTemplate;
import tk.mybatis.mapper.mapperhelper.SqlHelper;
import tk.mybatis.mapper.util.MetaObjectUtil;

/**
 * BaseDeleteMapper实现类,基础方法实现类
 *
 * @author liuzh
 */
public class BaseDeleteProvider extends MapperTemplate {

    public BaseDeleteProvider(Class<?> mapperClass, MapperHelper mapperHelper) {
        super(mapperClass, mapperHelper);
    }

    /**
     * 通过条件删除
     *
     * @param ms
     * @return
     */
    public String delete(MappedStatement ms) {
        Class<?> entityClass = getEntityClass(ms);
        StringBuilder sql = new StringBuilder();
        //如果设置了安全删除,就不允许执行不带查询条件的 delete 方法
        if (getConfig().isSafeDelete()) {
            sql.append(SqlHelper.notAllNullParameterCheck("_parameter", EntityHelper.getColumns(entityClass)));
        }
        // 如果是逻辑删除,则修改为更新表,修改逻辑删除字段的值
        if (SqlHelper.hasLogicDeleteColumn(entityClass)) {
            sql.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
            sql.append("<set>");
            sql.append(SqlHelper.logicDeleteColumnEqualsValue(entityClass, true));
            sql.append("</set>");
            MetaObjectUtil.forObject(ms).setValue("sqlCommandType", SqlCommandType.UPDATE);
        } else {
            sql.append(SqlHelper.deleteFromTable(entityClass, tableName(entityClass)));
        }
        sql.append(SqlHelper.whereAllIfColumns(entityClass, isNotEmpty()));
        return sql.toString();
    }

    /**
     * 通过主键删除
     *
     * @param ms
     */
    public String deleteByPrimaryKey(MappedStatement ms) {
        final Class<?> entityClass = getEntityClass(ms);
        StringBuilder sql = new StringBuilder();
        if (SqlHelper.hasLogicDeleteColumn(entityClass)) {
            sql.append(SqlHelper.updateTable(entityClass, tableName(entityClass)));
            sql.append("<set>");
            sql.append(SqlHelper.logicDeleteColumnEqualsValue(entityClass, true));
            sql.append("</set>");
            MetaObjectUtil.forObject(ms).setValue("sqlCommandType", SqlCommandType.UPDATE);
        } else {
            sql.append(SqlHelper.deleteFromTable(entityClass, tableName(entityClass)));
        }
        sql.append(SqlHelper.wherePKColumns(entityClass));
        return sql.toString();
    }
}

可以看出,在 DeleteProvider 类中,分为两个实现方法:

  • delete ():通过条件删除
  • deleteByPrimaryKey ():通过主键删除

以上两个方法均使用 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();
}

delete () 条件判断

其中,delete () 方法会判断对象中的所有字段。

位于 tk.mybatis.mapper.mapperhelper.SqlHelper 中的 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;

    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();
}

deleteByPrimaryKey () 条件判断

其中,deleteByPrimaryKey () 方法会判断对象中的主键字段。

位于 tk.mybatis.mapper.mapperhelper.SqlHelper 中的 wherePKColumns () 方法:

/**
 * where主键条件
 *
 * @param entityClass
 * @param entityName
 * @param useVersion
 * @return
 */
public static String wherePKColumns(Class<?> entityClass, String entityName, boolean useVersion) {
    StringBuilder sql = new StringBuilder();
    boolean hasLogicDelete = hasLogicDeleteColumn(entityClass);

    sql.append("<where>");
    //获取全部列
    Set<EntityColumn> columnSet = EntityHelper.getPKColumns(entityClass);
    //当某个列有主键策略时,不需要考虑他的属性是否为空,因为如果为空,一定会根据主键策略给他生成一个值
    for (EntityColumn column : columnSet) {
        sql.append(" AND ").append(column.getColumnEqualsHolder(entityName));
    }
    if (useVersion) {
        sql.append(whereVersion(entityClass));
    }

    if (hasLogicDelete) {
        sql.append(whereLogicDelete(entityClass, false));
    }

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

以上会在项目启动时生成对应的 SQL 模板。

实际示例

准备工作

新建表名

delimiter $$

CREATE TABLE `book` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `author` varchar(50) NOT NULL,
  `description` varchar(1000) NOT NULL,
  `isbn` varchar(10) NOT NULL,
  `title` varchar(250) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1$$

对象类:

@Data
@Table(name="book")
public class Book {
    @Id
    @GeneratedValue(generator = "JDBC")
    private Long id;

    private String author;

    private String description;

    private String title;
}

删除模板

则在项目启动时生成对应的 SQL 删除模板。

通过断点我们可以得到如下结果。

delete () 删除模板如下:

DELETE FROM book 
<where>
    <if test="id != null"> AND id = #{id}</if>
    <if test="author != null"> AND author = #{author}</if>
    <if test="description != null"> AND description = #{description}</if>
    <if test="title != null"> AND title = #{title}</if>
</where>

deleteByPrimaryKey () 删除模板如下:

DELETE FROM book 
<where>
    AND id = #{id}
</where>

结论

从源码和运行结果可知:

  • delete () 在参数全 null 的情况下回删除全表!在使用时需要特别注意!
  • deleteByPrimaryKey () 方法在传入空参数时,则不存在该问题。

deleteByPrimaryKey 执行空参数日志如下:

c.j.m.BookMapper.deleteByPrimaryKey      : ==>  Preparing: DELETE FROM book WHERE id = ?
c.j.m.BookMapper.deleteByPrimaryKey      : ==> Parameters: null
c.j.m.BookMapper.deleteByPrimaryKey      : <==    Updates: 0