MyBatis 复杂 SQL 很灵活,自研 ORM 为何仍要保留 SQL 逃生口?
摘要: MyBatis 的优势之一是复杂 SQL 可以明确交给开发者,自研 ORM 不应为了"全自动"丢掉这种能力。MetaLite ORM 用
BaseEntityDao覆盖单表高频 CRUD,同时保留BaseSqlDao作为联表、窗口函数和数据库特性的 SQL 逃生口,并对参数顺序、空结果和日志边界作出约束。
ORM 设计经常走向两个极端:
- 所有查询都手写 SQL,简单 CRUD 也充满重复字符串;
- 试图用一套 DSL 表达全部 SQL,最后抽象比 SQL 本身更难理解。
MetaLite 没有让 Criteria 无限膨胀,而是保留两条数据访问通道:高频单表操作走 BaseEntityDao,复杂查询走 BaseSqlDao。
一、BaseEntityDao 负责哪些高频问题
BaseEntityJdbcDao 根据实体元数据生成常见 SQL:
- 按主键或条件查询;
- 列表、排序、分页;
- 新增、批量新增;
- 按字段集合更新;
- 删除、计数和存在判断。
字段名可通过方法引用解析,值使用预编译参数。这部分价值不是"消灭 SQL",而是让重复率最高、最容易写错的单表操作使用统一路径。
文章 014 已详细拆解这条生成链。
二、哪些场景应该主动离开通用 CRUD
遇到以下需求时,继续扩展 Criteria 往往得不偿失:
- 多表关联和复杂子查询;
- 窗口函数、CTE、数据库专属函数;
- 一次返回非实体结构的统计报表;
- 精细控制执行计划或索引提示;
- 复杂批量 DML。
BaseSqlDao 为这些场景提供 insert、update、delete、count、单行和列表查询。它不是 ORM 失败后的补丁,而是刻意保留的 SQL 逃生口。
三、复杂 SQL 仍然复用哪些底座能力
BaseSqlJdbcDao 并非直接暴露一个裸 JdbcTemplate。它仍然复用:
@Dao指定的数据源组;DbRouter的读写选择;- 事务内强制读主库;
- DAO 切面日志配置;
- 数据访问异常转换;
- Map 到 Bean 的统一转换。
因此业务虽然显式写 SQL,连接池、主从、事务和异常治理仍在同一框架边界内。
四、LinkedHashMap 的 key 并不是命名参数
SQL 参数通过 LinkedHashMap<String, Object> 传入,但实现只读取 values():
java
List<Object> valueList = new ArrayList(paramMap.values());
这意味着 Map 的 key 只提高调用处可读性,不会按名称绑定 SQL。真正决定第一个 ? 对应哪个值的是插入顺序。
如果调用者换成普通 HashMap,或重构时调整 put 顺序,SQL 仍能执行,却可能把值绑定到错误字段。
所以这条 API 的准确契约是"有顺序的参数列表",而不是"命名参数"。更稳妥的演进方向是显式 List、参数对象或 Spring NamedParameterJdbcTemplate。
五、预编译参数和原始 SQL 不能混用
单条 DML 和查询支持 ? 占位符,可以避免把用户输入直接拼进 SQL。
但 batchInsertBySql(List<String>) 接收的是完整 SQL 字符串列表,没有参数绑定。只要这些字符串包含外部输入,就重新引入 SQL 注入和转义风险。
逃生口的原则应是:结构可以显式写,值仍尽量参数化。只有完全受控、没有外部数据的固定 SQL 才适合直接批量执行。
六、findOne 当前有一个空结果边界
findOneBySql 先调用列表查询,再直接执行 getFirst():
java
List<Map<String, Object>> list = findListBySql(sql, paramMap);
return list == null ? null : list.getFirst();
JdbcTemplate.queryForList 在没有记录时通常返回空列表而不是 null,因此这里可能抛出空列表异常,而不是返回 null。
此外方法不会自动追加 LIMIT 1,调用者必须自己限制结果。文章示例不能把它描述成和 findOneByCriteria 完全相同的语义。
七、SQL 日志为什么既有用又危险
开启 DAO SQL 打印后,JdbcHelper.formatSql 会把占位值替换进日志文本,排查路由和参数问题非常直观。
但这也可能记录手机号、证件号、Token 或大字段。格式化日志仅用于展示,并不是数据库最终执行文本,也没有完整处理引号转义。
生产环境应控制开关、字段和日志访问权限,不能把"便于排查"变成敏感数据副本。
八、GroupBy 已有,聚合条件仍未完成
公共 Query 支持 GroupBy,JDBC 查询会生成 GROUP BY。但 AggCriteria 当前仍是空类,尚未形成 HAVING、聚合函数和结果映射能力。
因此可以说"支持按字段分组",不能进一步宣传成"完整聚合 DSL"。复杂统计仍应走显式 SQL,直到聚合抽象拥有真实实现和测试。
九、逃生口也要有使用边界
一个健康的数据访问层通常分三层:
- 高频单表 CRUD 使用类型安全抽象;
- 复杂查询使用显式、参数化 SQL;
- 极少数性能热点允许数据库专属优化,并用测试固定行为。
MetaLite 同时保留 BaseEntityDao 和 BaseSqlDao,避免为了"统一"而牺牲可读性。真正需要继续治理的是让 SQL 逃生口的参数、空结果和日志契约更明确。
框架简介
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