核心概念

  • 通用 MapperBaseMapper<T> 接口提供了 deleteById, delete, deleteBatchIds, deleteByMap 等通用删除方法,无需手写 SQL 即可实现基础删除。
  • 物理删除 vs 逻辑删除
    • 物理删除:直接从数据库表中移除记录(DELETE FROM ...)。这是默认行为。
    • 逻辑删除:不真正删除数据,而是通过更新一个“删除状态”字段(如 deleted=1)来标记记录为已删除。查询时自动过滤已删除记录。需通过 @TableLogic 注解和全局配置启用。
  • 方法区别
    • deleteById(ID id):根据主键 ID 删除单条记录。
    • delete(Wrapper<T> wrapper):根据复杂条件(Wrapper)删除多条记录。
    • deleteBatchIds(Collection<? extends Serializable> idList):根据主键 ID 集合批量删除多条记录。
    • deleteByMap(Map<String, Object> columnMap):根据列名和值的 Map 进行删除(相当于 WHERE column1=value1 AND column2=value2 ...)。

操作步骤 (非常详细)

前提:已正确配置 MyBatis-Plus、数据源、Mapper 接口继承 BaseMapper<T>,并注入 Mapper Bean。

场景 1: 根据 ID 删除单条记录 (deleteById)

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper; // 注入 Mapper

    /**
     * 根据用户 ID 删除用户
     * @param userId 用户 ID
     * @return 删除的记录数 (通常为 0 或 1)
     */
    public int deleteUserById(Long userId) {
        // 1. 调用 BaseMapper 的 deleteById 方法
        int result = userMapper.deleteById(userId);
        // 2. result 为删除的行数
        if (result > 0) {
            System.out.println("成功删除用户,ID: " + userId);
        } else {
            System.out.println("未找到用户或删除失败,ID: " + userId);
        }
        return result;
    }
}

场景 2: 根据条件删除多条记录 (delete)

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 删除指定邮箱域名下的所有用户
     * @param emailDomain 邮箱域名,如 "example.com"
     * @return 删除的记录数
     */
    public int deleteUserByEmailDomain(String emailDomain) {
        // 1. 创建 QueryWrapper,构建删除条件
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        // 2. 添加条件:email 字段以指定域名结尾
        wrapper.likeRight("email", "@" + emailDomain); // 或使用 lambda: wrapper.lambda().likeRight(User::getEmail, "@" + emailDomain);
        // 3. 调用 delete 方法
        int result = userMapper.delete(wrapper);
        System.out.println("成功删除 " + result + " 个用户,邮箱域名: " + emailDomain);
        return result;
    }

    /**
     * 删除状态为 INACTIVE 且创建时间早于某个日期的用户
     * @param thresholdDate 日期阈值
     * @return 删除的记录数
     */
    public int deleteInactiveOldUsers(LocalDateTime thresholdDate) {
        LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
        lambdaWrapper.eq(User::getStatus, "INACTIVE")
                     .lt(User::getCreateTime, thresholdDate);
        return userMapper.delete(lambdaWrapper);
    }
}

场景 3: 根据 ID 集合批量删除 (deleteBatchIds)

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 批量删除用户
     * @param userIdList 用户 ID 列表
     * @return 删除的记录数
     */
    public int deleteUsersBatch(List<Long> userIdList) {
        // 1. 检查集合是否为空
        if (userIdList == null || userIdList.isEmpty()) {
            System.out.println("ID 列表为空,无需删除。");
            return 0;
        }
        // 2. 调用 deleteBatchIds 方法
        int result = userMapper.deleteBatchIds(userIdList);
        System.out.println("成功批量删除 " + result + " 个用户。");
        return result;
    }
}

场景 4: 根据列值 Map 删除 (deleteByMap)

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 删除指定部门和职位的用户
     * @param department 部门
     * @param position 职位
     * @return 删除的记录数
     */
    public int deleteUserByDeptAndPosition(String department, String position) {
        // 1. 创建 Map 存储列名和值
        Map<String, Object> columnMap = new HashMap<>();
        columnMap.put("department", department);
        columnMap.put("position", position);
        // 2. 调用 deleteByMap 方法
        // 注意:Map 的 key 是数据库列名 (非 Java 属性名),除非 map-underscore-to-camel-case=false
        int result = userMapper.deleteByMap(columnMap);
        System.out.println("成功删除 " + result + " 个用户,部门: " + department + ", 职位: " + position);
        return result;
    }
}

常见错误

  1. NullPointerException:
    • 原因userMapper 未被正确注入(@Autowired 失败)。
    • 解决:确保 Service 类被 @Service 注解,Mapper 被正确扫描(@MapperScan),且包路径正确。
  2. 删除了意外的记录:
    • 原因delete(Wrapper<T>)deleteByMap 构建的条件不精确或有误。
    • 解决在执行删除前,务必先用 selectList(wrapper)selectCount(wrapper) 测试查询条件是否正确! 仔细检查 wrapper 的拼接逻辑。
  3. deleteBatchIds 删除数量不符:
    • 原因idList 中包含不存在的 ID 或 null 值。
    • 解决deleteBatchIds 会忽略 null,并对每个 ID 执行删除,返回实际删除的行数。预期删除数可能小于 idList 大小。检查 ID 是否存在。
  4. 逻辑删除未生效:
    • 原因:未正确配置逻辑删除(全局配置 logic-delete-field 和实体类 @TableLogic),或 MybatisPlusInterceptor 未包含 BlockAttackInnerInterceptor (防止全表删除) 导致物理删除被阻止。
    • 解决:检查 application.yml 中的 mybatis-plus.global-config.db-config 相关配置和实体类 @TableLogic 注解。确认插件配置。
  5. deleteByMap 使用 Java 属性名:
    • 原因columnMap 的 key 使用了 Java 属性名(如 userName),但数据库列名是 user_name,且 map-underscore-to-camel-casetrue 时,MyBatis-Plus 可能无法正确映射。
    • 解决deleteByMap 的 key 必须使用数据库列名(如 user_name),除非明确知道映射规则。使用 QueryWrapper 通常更安全。
  6. 误删全表 (全表删除):
    • 原因:调用 delete(new QueryWrapper<>()) 且未启用 BlockAttackInnerInterceptor
    • 解决绝对避免创建一个无任何条件的 QueryWrapper 并用于删除!生产环境务必启用 BlockAttackInnerInterceptor 插件来阻止全表更新/删除。

注意事项

  1. 返回值:所有删除方法均返回 int 类型,表示数据库中实际被删除(或标记删除)的行数。不等于 0 表示成功删除至少一行。
  2. 逻辑删除优先:如果启用了逻辑删除,上述所有删除方法都会自动转换为 UPDATE 语句,更新 logic-delete-field 字段的值,而不是执行 DELETE。查询时也会自动添加 WHERE deleted=0 条件。
  3. deleteByMap 的局限性:只能构建 AND 连接的等值 (=) 条件。无法构建 ORLIKE、范围查询等复杂条件。对于复杂条件,强烈推荐使用 delete(Wrapper<T>)
  4. deleteBatchIds 的性能:底层通常生成 DELETE FROM table WHERE id IN (?, ?, ?) 语句,性能较好。但 idList 过大(如超过几千)时需注意数据库 IN 子句的长度限制和性能。
  5. 事务管理:删除操作通常需要事务保证。在 @Service 方法上添加 @Transactional 注解。
  6. 主键类型deleteByIddeleteBatchIds 的 ID 类型需与实体类 @TableId 定义的类型匹配(如 Long, String)。
  7. Wrapper 的泛型:创建 QueryWrapper 时,指定泛型 QueryWrapper<User> 能获得更好的类型安全和 Lambda 表达式支持。

使用技巧

  1. LambdaWrapper:优先使用 LambdaQueryWrapper<User>,避免硬编码字符串,提高代码可读性和重构安全性。
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(User::getId, userId).eq(User::getStatus, "ACTIVE");
    userMapper.delete(wrapper);
    
  2. 条件预检查:对于 deletedeleteByMap,先用 selectCount 确认要删除的记录数,避免误操作。
    long count = userMapper.selectCount(wrapper);
    if (count > 0 && confirmDeletion(count)) { // 业务确认逻辑
        userMapper.delete(wrapper);
    }
    
  3. 结合 @Transactional:在 Service 方法上使用 @Transactional(rollbackFor = Exception.class) 确保操作的原子性。
  4. 处理返回值:检查返回的 int 值,根据业务需求判断是否删除成功(如预期删除 1 行但返回 0,可能表示数据不存在或已被删除)。
  5. wrapper.last() 谨慎使用:虽然可以用 wrapper.last("LIMIT 1") 限制删除数量,但可能破坏分页等插件功能,且不够直观,不推荐用于删除。

最佳实践与性能优化

  1. 首选 deleteByIddeleteBatchIds:对于基于主键的删除,它们最直接、高效且安全。
  2. 慎用 deleteByMap:仅在条件简单且明确为等值 AND 时使用。复杂条件一律使用 Wrapper
  3. delete(Wrapper) 用于复杂条件:这是处理非主键复杂删除条件的标准方式。
  4. 强制启用 BlockAttackInnerInterceptor生产环境必须配置,防止因代码 Bug 导致全表数据被清空。
    @Configuration
    public class MyBatisPlusConfig {
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 防止全表删除/更新
            // ... 其他插件
            return interceptor;
        }
    }
    
  5. 批量删除大集合
    • 如果 idList 非常大(如数万条),考虑分批处理(例如每次 1000 条),避免单次 SQL 过长或事务过大。
    • 或者,如果条件能用 SQL 表达,优先使用 delete(Wrapper) 一条 SQL 完成。
  6. 性能监控:关注删除操作的执行时间,特别是 delete(Wrapper) 的复杂查询条件,确保相关字段有合适的索引。
  7. 软删除(逻辑删除)策略
    • 除非有明确的合规或审计要求必须物理删除,否则优先考虑逻辑删除,避免数据丢失。
    • 定期归档或物理清理(DELETE)已逻辑删除且过期很久的数据,以控制表大小。
  8. 日志记录:在执行重要删除操作前后,记录日志(如删除的 ID、数量、操作人),便于审计和问题排查。