Spring JDBC 被低估了吗?—— 关于 MyBatis 与 Spring JDBC 复杂度的祛魅
在互联网的后端技术社区中,有一个近乎"政治正确"的共识:MyBatis 比 Spring JDBC 更简单、更高效,尤其适合复杂的 SQL 场景。
然而,当我们剥离营销话术、抛开历史包袱,单纯从工程实现、代码复杂度、调试成本和心智负担这几个维度重新审视这两者时,会发现一个被严重低估的事实:在大多数场景下,MyBatis 并没有比 Spring JDBC 更简单,甚至在"简洁性"和"诚实性"上,MyBatis 是一种系统性的倒退。
本文不引入任何第三方增强框架(如 MyBatis-Plus),仅对比原生 MyBatis 与原生 Spring JDBC。
一、200 行的实证:一个工具类就让 MyBatis 没有还手之力
在展开理论分析之前,先看一组可运行的成品代码——Spring JDBC ++,一个基于 Spring JDBC 的极简工具类集合。
共 4 个类,合计约 200 行:
| BaseCondition | 条件类,管理 WHERE 拼接和参数收集 | ~95 行 |
| BaseDao | DAO 基类,封装 list/page/row/columns/field | ~95 行 |
| Page | 分页结果包装类 | ~25 行 |
| Sql | SQL 工具:countSql 生成 + SQL 清洗 | ~25 行 |
以下是完整源码,逐类说明:
1. BaseCondition —— 条件层
条件类,使用模板方法模式。子类重写 addCondition(),在其中调用 add() / and() 方法声明业务条件。框架自动处理判空、拼接、参数收集。
package com.gzz.common;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import lombok.Getter;
import lombok.Setter;
/**
* 条件基类 —— 模板方法模式
*/
public abstract class BaseCondition {
@Getter @Setter
private Integer size = 10; // 页大小
@Getter @Setter
private Integer page = 1; // 当前页
protected List<Object> paramList = new ArrayList<>();
protected StringBuilder condition = new StringBuilder();
/** 子类实现:声明业务条件 */
protected abstract void addCondition();
/** 输出 AND 片段(不含 WHERE 关键字),用于子查询或拼接 */
public String and() {
condition.setLength(0);
paramList.clear();
addCondition();
return condition.toString();
}
/** 输出 WHERE 完整条件,用于主查询 */
public String where() {
return and().replaceFirst("AND", "WHERE");
}
/** 获取参数数组 */
public Object[] array() {
return paramList.toArray();
}
/** IN 条件的辅助方法:生成 (?,?,?) 占位符 */
private static String in(final Object... ids) {
return String.format("(%s)", String.join(",", Collections.nCopies(ids.length, "?")));
}
// ========== 5 个 add 重载,覆盖全部条件声明场景 ==========
/** 单值条件:值非空才拼接,自动判空 */
protected final void add(String sql, Object value) {
if (Objects.nonNull(value) && StringUtils.hasText(value.toString())) {
condition.append(" " + sql);
paramList.add(value);
}
}
/** IN 条件:数组自动展开为 (?,?,?) */
protected final void add(final String sql, final Object[] values) {
if (StringUtils.hasText(sql) && !ObjectUtils.isEmpty(values)) {
condition.append(" " + sql).append(in(values));
paramList.addAll(Arrays.asList(values));
}
}
/** LIKE 条件:site=1左%, 2右%, 3前后% */
protected final void add(final String sql, final String value, final int site) {
if (Objects.nonNull(sql) && StringUtils.hasText(value)) {
condition.append(" " + sql);
switch (site) {
case 1 -> paramList.add("%" + value);
case 2 -> paramList.add(value + "%");
case 3 -> paramList.add("%" + value + "%");
}
}
}
/** 逻辑开关:按 boolean 决定是否拼接 */
protected final void add(final String sql, boolean logic) {
if (logic && Objects.nonNull(sql) && StringUtils.hasText(sql)) {
condition.append(" " + sql);
}
}
/** 无条件直接追加 */
protected final void add(final String sql) {
add(sql, true);
}
}
2. BaseDao —— 执行层
DAO 基类,封装了四个核心查询形态(list / row / columns / field)和分页方法。所有 SQL 通过 JdbcTemplate 执行,BeanPropertyRowMapper 自动映射结果到实体/VO。
package com.gzz.common;
import static com.gzz.common.Sql.wash;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.SingleColumnRowMapper;
/**
* DAO 基类:4 种查询形态 + 分页,覆盖全部结果集场景
* list() – 多行多列 → List<T>
* row() – 单行多列 → T
* columns() – 多行单列 → List<T>
* field() – 单行单列 → T
*/
public class BaseDao {
@Autowired
protected JdbcTemplate jdbc;
// ========== 4 种查询形态 ==========
/** 多行多列:列表查询 */
protected <T> List<T> list(String sql, Class<T> clazz, Object... obj) {
return jdbc.query(sql, new BeanPropertyRowMapper<>(clazz), obj);
}
protected <T> List<T> list(String sql, Class<T> clazz, BaseCondition cond) {
return jdbc.query(sql, new BeanPropertyRowMapper<>(clazz), cond.array());
}
/** 单行多列:查询单条记录 */
protected <T> T row(String sql, final Class<T> clazz, final Object... obj) {
return jdbc.queryForObject(sql, new BeanPropertyRowMapper<>(clazz), obj);
}
protected <T, C extends BaseCondition> T row(String sql, final Class<T> clazz, final C cond) {
sql += cond.where();
return row(sql, clazz, cond.array());
}
/** 多行单列:查询一列数据(如 ID 列表) */
protected <T> List<T> columns(final String sql, final Class<T> clazz, final Object... obj) {
return jdbc.query(sql, new SingleColumnRowMapper<>(clazz), obj);
}
protected <T, C extends BaseCondition> List<T> columns(String sql, final C cond, final Class<T> clazz) {
sql += cond.where();
return columns(sql, clazz, cond.array());
}
/** 单行单列:查询标量值(COUNT / SUM / MAX) */
protected <T> T field(String sql, final Class<T> clazz, final Object... obj) {
return jdbc.queryForObject(sql, clazz, obj);
}
protected <T, C extends BaseCondition> T field(String sql, final Class<T> clazz, final C cond) {
sql += cond.where();
return field(sql, clazz, cond.array());
}
/** 计数辅助方法 */
protected Integer count(String sql, final Object... obj) {
return field(sql, Integer.class, obj);
}
// ========== 分页(基于 list + field 的组合派生) ==========
/**
* 标准分页:自动生成 COUNT SQL(替换法)
* 适用于大多数场景(不含子查询/UNION 干扰)
*/
protected <T, C extends BaseCondition> Page<T> page(String sql, final C cond, final Class<T> clazz) {
sql = wash(sql);
String countSql = Sql.countSql(sql) + cond.where();
Integer count = count(countSql, cond.array());
int offset = (cond.getPage() – 1) * cond.getSize();
String pageSql = sql + cond.where() + " LIMIT " + offset + "," + cond.getSize();
List<T> data = list(pageSql, clazz, cond.array());
return new Page<>(data, cond.getSize(), count, cond.getPage());
}
/**
* 兜底分页:子查询法套 COUNT
* 适用于含 UNION / 复杂子查询 等 countSql 无法自动处理的极端场景
*/
protected <T, C extends BaseCondition> Page<T> page0(String sql, final C cond, final Class<T> clazz) {
String countSql = "SELECT COUNT(1) FROM (" + sql + cond.where() + ") t";
Integer count = count(countSql, cond.array());
int offset = (cond.getPage() – 1) * cond.getSize();
String pageSql = sql + cond.where() + " LIMIT " + offset + "," + cond.getSize();
List<T> data = list(pageSql, clazz, cond.array());
return new Page<>(data, cond.getSize(), count, cond.getPage());
}
}
3. Page —— 分页结果包装类
仅包含四个字段:数据列表、每页大小、总记录数、当前页码。无框架侵入,纯 POJO。
package com.gzz.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 分页结果包装类
*/
@Setter
@Getter
@AllArgsConstructor
public class Page<T> {
private List<T> dataList; // 当前页数据
private Integer size; // 每页记录数
private Integer rowCount; // 总记录数
private Integer page; // 当前页码
}
4. Sql —— SQL 工具类
提供两个核心功能:自动生成 COUNT(1) SQL(找第一个不在括号内的 FROM),以及 SQL 文本清洗。
package com.gzz.common;
/**
* SQL 处理工具
*/
public final class Sql {
/** 清洗 SQL:去除多余换行、空格、逗号前后的空格 */
public static String wash(String sql) {
return sql.replaceAll("\\n", " ")
.replaceAll("\\\\s+", " ")
.replaceAll(",\\\\s+|\\\\s+,", ",");
}
/**
* 自动生成 COUNT SQL
* 思路:找第一个不在括号内的 FROM,替换为 SELECT COUNT(1)
* 不支持子查询/UNION 时请使用 page0 兜底
*/
public static String countSql(String sql) {
int bracketCount = 0;
for (int i = 0; i < sql.length() – 3; i++) {
char c = sql.charAt(i);
switch (c) {
case '(' -> bracketCount++;
case ')' -> bracketCount = Math.max(0, bracketCount – 1);
default -> {
if (bracketCount == 0 && (c == ' ') &&
(sql.regionMatches(true, i, " FROM ", 0, 6) ||
sql.regionMatches(true, i, " FROM(", 0, 6))) {
return "SELECT COUNT(1) " + sql.substring(i);
}
}
}
}
throw new IllegalArgumentException("无法自动生成count语句,请使用page0方法。SQL: " + sql);
}
}
使用示例:UserDao 继承 BaseDao
业务 DAO 只需继承 BaseDao,空类即获得全部能力:
package com.gzz.sys.user;
import com.gzz.common.BaseDao;
import com.gzz.common.Page;
import org.springframework.stereotype.Repository;
@Repository
public class UserDao extends BaseDao {
/** 联表分页查询示例 */
public void demo01() {
UserCond cond = UserCond.builder().userId(–1L).age(10).deptName("全真教").build();
String sql = """
SELECT u.user_id, u.user_name, u.age, d.name dept_name
FROM sys_user u
JOIN sys_dept d ON d.id = u.dept_id""";
Page<UserVo> page = page(sql, cond, UserVo.class);
// 返回分页结果,dataList 已自动映射到 UserVo
}
}
小结
这 4 个类、约 200 行代码,实现了:
- 动态条件声明:add("AND name = ?", name) 一行代替 if + 拼接 + 参数收集
- 单表联表同一套 API:list() / page() 既可用于单表,也可用于联表
- 4 种查询形态 + 分页:覆盖全部结果集场景,不重叠不遗漏
- 自动 COUNT 生成:Sql.countSql() 自动解析 FROM 位置
MyBatis 的数万行代码(核心框架 + XML 解析引擎 + OGNL 引擎 + 拦截器 SPI),本质上是把这 200 行工具类覆盖的能力,用一套极其复杂的私有体系重新实现了一遍。
这 200 行代码的存在,本身就是一个实证:持久层框架的复杂度,有多少是必要的,有多少是被“重新发明”出来的。
二、简洁性的假象:字符数与噪音比的真相
评判一个 API 是否简洁,最直观的指标不是"看起来是否整齐",而是信噪比(Signal-to-Noise Ratio)。
1. MyBatis 的"噪音"成本
一个典型的 MyBatis Mapper 片段如下:
<mapper namespace="com.example.UserMapper">
<select id="find" resultType="User">
SELECT id, name FROM user
<where>
<if test="name != null">
AND name = #{name}
</if>
</where>
</select>
</mapper>
除了 SQL 本身,开发者必须维护 namespace、id、resultType、<where>、<if> 等大量 XML 元数据。这些代码对数据库执行毫无意义,仅仅是框架的"税收"。在复杂的业务 Mapper 中,XML 标签和属性带来的噪音占比往往超过 70%。
2. Spring JDBC 的"样板"真相
反观 Spring JDBC:
String sql = "SELECT id, name FROM user WHERE name = ?";
jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), name);
虽然存在 BeanPropertyRowMapper 这样的"样板"代码,但这属于 Java 语言层面的标准结构,IDE 可以完美识别、重构和编译期校验。它没有引入任何新的语法,也没有额外的配置文件。
结论: MyBatis 的简洁是"删除了换行和引号的简洁",而 Spring JDBC 的冗长是"Java 语法本身的冗长"。前者是噪音,后者是透明的结构。
三、昂贵的学费:为了解决"自己"的问题
MyBatis 的高复杂度并非偶然,而是其设计架构的必然产物。为了支撑其运行模式,它被迫引入了一套庞大的"自举体系"。
开发者在使用 MyBatis 时,必须首先掌握一整套与 SQL 无关的"元知识":
- Mapper 接口与 XML 的绑定机制:包括 XML 头、namespace、接口 ID 的映射规则
- 庞大的配置元素:上百个 XML 属性、几十种标签(如 resultMap、association、collection)
- OGNL 表达式语言:为了在 XML 中实现逻辑判断,开发者必须学习 OGNL 这一特定领域的表达式语言,它既不像 SQL,也不像 Java
- 专有异常体系:几十种 MyBatis 特有的异常类,掩盖了底层 JDBC 异常的原始面貌
最关键的批判在于:这些复杂的概念(Namespace、OGNL、XML 标签)全都是在解决 MyBatis 自身的问题,而不是解决数据库访问的问题。
更令人费解的是,MyBatis 选择 XML 作为模板语言。纵观软件开发史,模板语言(如 JSP、Velocity、FreeMarker)的设计初衷是为了生成面向人类阅读的文档(如 HTML)。它们擅长处理排版、格式和文本替换,但天生不适合生成面向机器解析的、结构严格的 SQL。用模板语言来拼接 SQL,无异于用排版工具去编写汇编代码——这本身就是一种范畴错误(Category Mistake)。这也解释了为什么 MyBatis 的配置如此繁琐且脆弱:它在强迫一个错误的工具去做一件它不擅长的事。
相比之下,Spring JDBC 没有任何学习成本。你只需要懂 SQL 和 Java。所有的"框架知识"不过是 JdbcTemplate 的几个方法签名。
四、动态 SQL:语法迁移而非抽象升华
MyBatis 引以为傲的"动态 SQL",常被视作其杀手锏。但冷静分析,<if>、<foreach> 这些标签真的比 Java 原生的 if 和 for 更优越吗?
1. 逻辑等价,成本更高
MyBatis 的做法是将 Java 逻辑迁移到 XML 中:
<if test="ids != null">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
这实际上是用一套复杂的 XML 标签语言(OGNL)重写了 Java 已经具备的逻辑判断能力。这不仅增加了学习成本(OGNL 语法),还切断了调试的可能性——你无法在 XML 中对 <if> 标签打一个断点。
2. Spring JDBC 的直接性
Spring JDBC 直接使用 Java 语言进行判断:
StringBuilder sql = new StringBuilder("SELECT * FROM user WHERE 1=1");
List<Object> params = new ArrayList<>();
if (name != null) {
sql.append(" AND name = ?");
params.add(name);
}
// …
jdbcTemplate.query(sql.toString(), new BeanPropertyRowMapper<>(User.class), params.toArray());
这种方式虽然看起来"粗糙",但逻辑完全透明。开发者使用的是全功能的 Java 语言,而不是 XML 的子集。
结论: MyBatis 的动态 SQL 只是将逻辑从 Java 代码"平移"到了 XML 中,并未提供实质性的抽象升华,反而牺牲了可读性和可调试性。
五、调试与扩展:白盒与黑盒的博弈
这是两者最本质的区别。
1. MyBatis:黑盒拦截的复杂性
在 MyBatis 中,如果你想对 SQL 执行过程进行干预(如分表、审计、逻辑删除),你必须面对其插件机制。编写一个简单的拦截器,通常需要:
- 理解 Interceptor 接口
- 掌握 Invocation 对象
- 熟练使用 MetaObject 反射工具
- 深入 MyBatis 内部的 Executor、StatementHandler 等核心对象
即便是一个简单的分页插件,其代码量也可能超过 100 行,且充满了反射调用。这使得 MyBatis 的扩展门槛极高,通常只有资深架构师才能完成,普通开发者只能被动依赖现成的插件(如 PageHelper)。
2. Spring JDBC:AOP 的透明性
Spring JDBC 的扩展极其简单。由于 SQL 执行完全暴露在 Java 方法中,你可以使用 Spring AOP 或简单的装饰器模式进行拦截:
@Aspect
@Component
public class SqlLoggingAspect {
@Before("execution(* org.springframework.jdbc.core.JdbcTemplate.*(String, ..))")
public void logSql(JoinPoint jp) {
// 直接获取 SQL 和参数
}
}
这种扩展方式对实习生来说 20 分钟即可掌握。它不需要理解任何框架内部实现,仅仅是对方法调用的拦截。
结论: MyBatis 的插件生态看似繁荣,实则是由于其内部黑盒设计导致扩展困难,迫使社区产生了大量复杂的"补丁"。而 Spring JDBC 几乎没有插件生态,是因为它根本不需要——它的白盒设计使得扩展成本极低。
六、ORM 的幻象:0 ORM 与伪解耦
社区常称 MyBatis 为"半 ORM"框架,但这其实是一种误读。
1. 0 ORM 的本质
ORM 的核心在于"对象状态管理"和"自动脏检查"。MyBatis 显然不具备这些能力。它的 ResultMap 仅仅是字段映射(Field Mapping),而非关系映射(Relationship Mapping)。它只是把 ResultSet 的数据搬运到 Java 对象中,两者之间不存在任何生命周期的关联。因此,MyBatis 实际上是 0 ORM。
2. 伪解耦
MyBatis 常被吹捧为"解耦 SQL"。但事实是,业务逻辑调用 SQL 是强依赖,无法解耦。MyBatis 所做的,只是将 SQL 从 Java 代码移到了 XML 文件中。这非但没有解耦,反而引入了三层耦合:Java 接口 → XML ID → SQL。当 SQL 发生变化时,你需要同时维护 Java 接口签名和 XML 中的 SQL 片段,耦合度反而增加了。
相比之下,Spring JDBC 坦率地承认了这种依赖:SQL 就在代码里,修改 SQL 就是修改代码。这种"诚实"大大降低了维护的认知成本。
七、生态基石:被忽视的事实
一个常被忽视的现实是:Spring JDBC 是整个 Spring 数据访问层的基石。
无论上层是 JPA、Hibernate 还是 MyBatis,只要运行在 Spring 容器中,最终都依赖于 Spring JDBC 提供的:
- DataSource 抽象
- 事务管理(PlatformTransactionManager)
- 异常转译(SQLExceptionTranslator)
- 连接池管理
MyBatis 需要通过 mybatis-spring 桥接层来适配 Spring JDBC 的基础设施。这意味着,在 Spring 生态中,Spring JDBC 是"内核",其他框架是运行在内核之上的"应用"。许多号称 MyBatis 独有的高级特性(如分库分表、读写分离),其底层无一例外都是基于 Spring JDBC 的 DataSource 代理机制实现的。
八、多维度横向对比:MyBatis 仅剩一项微弱优势
把持久层框架的选择拉回到工程决策的基准面上,我们需要一个更系统的比较框架。以下六个维度,基本覆盖了开发者在选型和日常使用中最关心的全部侧面:
| 学习成本 | 低(只需懂 SQL + Java) | 高(需学 XML、OGNL、Mapper 绑定、拦截器 SPI) | MyBatis 引入了一整套私有语法体系 |
| 代码信噪比 | 高(噪音仅 Java 语法本身) | 低(XML 标签、namespace、resultMap 等框架噪音占 70%+) | 噪音不是业务,是伺候框架 |
| 调试透明度 | 白盒(SQL 完整可见,断点可打) | 黑盒/半黑盒(占位符 SQL,拦截器改写不可见) | 线上排障时白盒方案优势巨大 |
| 扩展成本 | 低(Spring AOP 即可,20 分钟上手) | 高(需扒 Interceptor、BoundSql、MetaObject 内部对象) | MyBatis 的"生态繁荣"本质是补坑生态 |
| 运行时性能 | ≈ 原生 JDBC(链路最短) | 95% ~ 97%(有代理/反射/解析损耗) | 差值不大,但方向明确 |
| 业务代码浓度 | 高(SQL 直接可见,条件即业务) | 低(大量 XML 配置淹没 SQL 本体) | 代码里有多少行在真正描述业务,而非描述框架 |
这六个维度中,MyBatis 唯一可能占优的,仅仅是在"复杂动态 SQL"场景下的字符输入量——用 <if> 和 <foreach> 确实比手写 if (xx != null) sql.append(…) 少敲几个字符。但代价是:
- 你必须先花 2~5 天学 OGNL 和 XML 标签
- 你失去了断点调试的能力
- 你想加数据权限时得去写拦截器
- 你的日志里只有占位符,调试全靠手动替换 ?
- 你的业务代码浓度被 XML 噪音压到不足 30%
用"一丁点字符节省",换来认知负荷、调试成本和扩展门槛的系统性飙升。 这笔账在任何理性的工程决策里都算不过来。
而且,Spring JDBC 的那一丁点字符劣势,在 Java 17+ 的 Text Block 和 IDE 智能补全面前,几乎已经被抹平。即便不用任何封装,原生 Spring JDBC 在代码可读性上也不输 MyBatis 的 XML。
结论:MyBatis 在 6 个核心维度中,5 个全面落后,仅剩的 1 个微弱优势也不足以支撑"它是一个更简单或更好的框架"这一论断。
九、结语
MyBatis 的流行,很大程度上源于它迎合了对 SQL 控制欲强、却又不愿深入理解 JDBC API 的开发群体。它通过引入 XML 和 OGNL,将简单的 SQL 调用包装成了一门复杂的"配置艺术"。
然而,当我们回归工程本质,会发现:
Spring JDBC 之所以在社区声量较小,不是因为它不够强大,而是因为它太"老实"了——老实到没有坑,老实到不需要写教程,老实到让开发者必须直面 SQL 的本质。
在这个充斥着过度设计和复杂框架的时代,或许我们该重新审视这位低调的老兵。有时候,最简单、最直接的方法,恰恰是最优雅的解决方案。Spring JDBC 从未过时,它只是静静地等待着工程师们回归理性。
附录:Spring JDBC Ultra 框架五大独创亮点与主流框架对比
| 1. 单表/联表同一套 API | 采用统一的心智模型,单表与联表查询无缝切换,无需另起炉灶或推倒重来。 | 单表一套 API,联表必须换一套(如退回原生 SQL 或 XML),心智严重割裂。 |
| 2. 单表/联表同一套条件类 | 条件类设计不区分单表与联表,完全复用,极大简化了代码编写。 | 单表与联表通常需要不同的条件构建方式,增加了学习和维护成本。 |
| 3. 多组参数合并(多条件合并) | 原生支持多条件类合并,在复杂的报表、多维度过滤场景中特别实用且高效。 | 缺乏原生支持,通常需要开发者手动拼接 Map 或 Wrapper,维护成本高。 |
| 4. 屏蔽第一层 if | 彻底屏蔽了 90% 以上的第一层 if 判空逻辑,条件类内部统一处理,大幅简化业务代码。 | 动态条件拼接往往需要大量 if (xxx != null) 判空,代码冗余且极易出错。 |
| 5. 2×2×2×2 正交完备 API | 基于集合运算结果,抽象出 4 个象限(List / Columns / Row / Field),结合日志开关与参数类型进行正交重载,纯面向业务设计,不重叠不遗漏。 | API 设计通常面向 SQL 抽象(如 selectOne / selectList),缺乏这种数学上正交完备的极简设计。 |
这 5 个独创点,在以往的持久层框架里几乎很少见过。它们不是"语法糖",而是把"拼 SQL"这件事重新做了一次工程抽象:
- "单表和联表同一套条件类"与"同一套 API"放在一起,意味着开发者不需要在脑子里切换两套语境——写单表时是它,写五表 JOIN 时还是它,条件怎么加、参数怎么合并,手法完全一致。
- 分页调用的底层就是 list() + field() 的变体;除了新增、删除、修改、查询这四类基础操作,其余所有读取场景都支持可变参数与条件类的直接传入,没有中间转换层。
- 多组参数合并(mergeParams(main, sub))让"主表条件"和"子查询条件"在 Java 层面就是数组拼接,而不是 OGNL 里的 main.xxx 嵌套。
- 屏蔽第一层 if 之后,业务代码里不再出现 if (name != null) sql.append(…),这部分被判空逻辑收编进 add() / and() 内部,调用方只声明"要什么条件",不写"怎么判空"。
这些设计叠加起来,极大简化了开发:你面对的不再是"MyBatis 的 XML + Mapper 接口 + OGNL"三层语言,也不是"JPA 的 Entity + Repository + Specification"三层抽象,而是一层 Java、一层 SQL、一个条件类、一套 BaseDao 方法。
相关开源地址:





