Mybatis动态写操作防线-条件为空时阻止全表更新删除

动态写操作防线:条件为空时阻止全表更新与删除

事故防线: 动态 SQL 最危险的失败方式不是语法错误,而是条件被全部过滤后仍然生成合法的全表 UPDATEDELETE。本文从一个批量状态修改接口出发,拆解参数校验、Criteria 校验、影响行数、事务和审计五层防线,并结合 MetaLite 当前源码说明它已经拦住了什么、仍需要补上什么。

假设后台提供批量关闭订单接口:

closeOrders(tenantId, orderIds, reason);

最终生成:

UPDATE order_info
SET status = 'CLOSED'
WHERE tenant_id = ? AND id IN (...)

如果 tenantId 丢失、orderIds 为空,而动态 SQL 又把无效条件全部跳过,最终可能变成:

UPDATE order_info SET status = 'CLOSED'

这不是慢查询,而是数据事故。

一、为什么“对象不为 null”远远不够

很多公共 DAO 会做这样的校验:

assert criteria != null;

但下面这个对象同样不为 null

Criteria criteria = Criteria.where();

它是否包含有效条件,要看内部条件树,而不是 Java 引用是否存在。

同理,参数 DTO 存在也不代表关键字段有效:

  • 空字符串被 trim 后消失;
  • 空集合对应的 IN 被跳过;
  • 非法枚举被转换为 null
  • 权限过滤没有得到任何组织 ID;
  • 软删除和租户条件漏传。

二、MetaLite 当前已经具备的两层校验

MetaLite updateByCriteria 会检查 CriteriaUpdate 不为空,并要求更新字段集合非空:

public int updateByCriteria(Criteria criteria, Update update) {
    AssertUtil.paramNotNull(criteria, "criteria");
    AssertUtil.paramNotNull(update, "update");
    AssertUtil.paramNotEmpty(update.getSetMap(), "update.setMap");
    // 生成 UPDATE、SET、WHERE
}

它能够阻止:

  • 调用方完全没有传查询对象;
  • 没有任何更新字段却执行空更新;
  • IN 传入空集合,因为 Criteria.in 会提前拒绝。

deleteByCriteria 也要求查询对象不为空。

这些校验减少了低级错误,但当前源码仍有一个必须如实说明的边界:仅校验 Criterianull,不能证明其内部一定包含有效限制条件。

三、企业 ORM 需要显式的全表操作语义

通用 DAO 不应通过“猜测条件”决定能否全表更新。更可靠的设计是把危险操作变得显式。

例如默认拒绝空条件:

if (criteria.isEmpty()) {
    throw new IllegalStateException("禁止无条件更新");
}

确实需要全表操作时,不应该伪造一个恒真条件,而应使用明确入口:

dao.updateAll(update, FullTableOperationToken.confirmed());

这样代码搜索、审查和审计都能找到所有高风险操作。

四、只判断条件数量仍然不够

下面的条件数量不是零,但约束能力可能很弱:

WHERE status IS NOT NULL

它可能匹配 99% 的数据。因此完整防线还需要考虑影响范围。

1. 更新前预估数量

long count = orderDao.countByCriteria(criteria);
AssertUtil.stateCheck(count <= 1000, "单次最多处理1000条");

需要注意预估查询和真正更新之间存在并发时间窗,不能把它当成绝对锁。

2. 更新后检查影响行数

int affected = orderDao.updateByCriteria(criteria, update);
if (affected > expectedMax) {
    throw new IllegalStateException("影响行数超过阈值");
}

这段检查必须位于事务中,抛出异常后才能回滚。

3. 高风险操作分批执行

通过主键分页、任务批次和进度记录控制影响范围,避免一次事务锁住整张表。

五、租户条件和数据权限不能靠调用方自觉

最危险的遗漏往往不是业务条件,而是安全条件。

如果每个 Service 都手工追加:

criteria.eq(OrderEntity::getTenantId, tenantId);

迟早会有一个入口忘记。企业底座应考虑在 DAO、路由或统一查询构建阶段注入租户范围,并为绕过机制设置严格审计。

数据权限也是同样的问题:前端没有传组织 ID,不应解释为查询全部组织;权限计算得到空集合,也不应跳过条件。

六、删除比更新更需要恢复方案

更新错误有时还能根据旧字段恢复,物理删除往往无法逆转。高风险删除至少需要:

  • 优先逻辑删除;
  • 保存操作者、请求 ID 和条件摘要;
  • 删除前导出主键清单或快照;
  • 限制单次影响行数;
  • 在事务中执行影响行数检查;
  • 提供恢复脚本并演练。

“有数据库备份”不等于能快速恢复某次误删除。真正可用的恢复方案必须知道删了哪些行以及如何定位。

七、建议给 MetaLite 补上的公共能力

基于当前源码,下一步最有价值的增强不是再增加一个 CRUD 方法,而是补齐写安全策略:

  1. Criteria.isEmpty() 与有效条件数量;
  2. 默认禁止空条件更新和删除;
  3. 全表操作使用独立、显式 API;
  4. DAO 支持最大影响行数策略;
  5. 统一记录逻辑表、路由结果、条件摘要和影响行数;
  6. 为租户、数据权限条件提供不可遗漏的扩展点;
  7. 对批量操作提供分批和恢复模板。

这也是源码分析的价值:不是把现有实现包装成完美答案,而是明确已经建立的防线和仍需演进的边界。

八、上线前的事故预防清单

  • 空 DTO、空字符串、空集合均有测试;
  • Criteria 内无有效条件时默认拒绝执行;
  • 更新字段集合不能为空;
  • 租户与数据权限由公共机制注入;
  • 批量更新、删除设置影响行数阈值;
  • 阈值检查位于可回滚事务中;
  • 日志记录条件摘要而不泄露敏感值;
  • 物理删除具有主键清单、备份和恢复流程。

九、结论

动态 SQL 最可怕的不是报错,而是生成了一条完全合法、执行成功、影响全表的语句。

MetaLite 当前对空更新集合和空 IN 已经进行入口校验,但 Criteria 非空不等于查询范围安全。企业级 ORM 真正应该做到的是:默认拒绝模糊的危险行为,让全表操作显式化,让影响范围可度量,让事故能够回滚和恢复。


框架简介
MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。

源码基线
JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3,具体组件版本以项目 backend-bom 为准。

作者简介
15 年 Spring 体系企业级开发经验,专注于 Java 微服务架构、工程治理与生产实践。

持续更新
MetaLite 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。

在线演示
演示地址: https://admin.metalite.top/
演示账号: guess
演示密码: admin@2026

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值