持久层的设计哲学:从 MyBatis 的系统性缺陷到 Spring Jdbc Ultra 的范式革命

作者注:本文不是框架使用教程,是一次关于持久层设计范式的深度反思。基于十余年企业级开发实践,十余家企业、四五年的生产验证,以及一套名为 SimpleDAO(Spring Jdbc Ultra)的框架,我们来讨论一个被长期忽视的问题:持久层框架到底应该面向数据库设计,还是面向业务设计?


一、问题的提出:MyBatis 为什么流行?

MyBatis 是国内 Java 生态中最流行的持久层框架之一。但流行不等于正确。我们需要追问:它的流行是技术选择的结果,还是认知失调、历史包袱和惯性使然?

1.1 MyBatis 的"优势"说辞

互联网上为 MyBatis 辩护的常见论点:

  1. SQL 与代码分离,便于维护
  2. 动态 SQL 强大灵活
  3. 结果自动映射
  4. 缓存机制
  5. 插件扩展机制
  6. Mapper 接口,IDE 有代码提示
  7. XML 语法高亮
  8. 批量操作更优雅
  9. 存储过程调用支持更好
  10. 与 Spring 集成后事务管理更方便

1.2 逐一检验:站在 Spring JDBC 的参照系上

说辞 检验结论
SQL 与代码分离 伪优势 — 分离的是文件,不是关注点。XML 里 1/3 表达式、1/3 标签、1/3 SQL,三种语言耦合
动态 SQL Java 更优 — Java 字符串操作能力远强于 XML 标签
自动映射 Spring JDBC 更优BeanPropertyRowMapper 零配置,MyBatis 的 resultMap 是手工配置假装自动
缓存机制 鸡肋 — 用户第一件事就是关闭缓存
插件扩展 Spring AOP 更优 — 白盒扩展 vs 黑盒拦截器
Mapper 接口 伪抽象 — 无多实现场景,纯为框架代理机制服务
XML 语法高亮 无关 — Java Text Block 同样有 SQL 高亮
批量操作 Spring JDBC 更直接
存储过程 平手
Spring 事务 平手 — 两者都是 Spring 事务

结论:站在 Spring JDBC 的参照系上,MyBatis 没有任何站得住脚的优势。


二、MyBatis 的系统性缺陷

MyBatis 不是"有一点毛病",是全身都是毛病。这些毛病不是设计失误,是范式层面的错误

2.1 缺陷一:XML 拼字符串 — 用残缺的标签语言做 Java 原生就擅长的事

“你撕开 XML 那层皮,里面到底是什么?不还是一坨低级的字符串拼接吗?”

Java 里有 replaceformat、正则、流式处理,能把字符串玩出花来。XML 有什么?XML 连个像样的循环和字符串截取都写得费劲。

动态 SQL 的灵魂在于"逻辑",不在于"标签"。为什么换个文件,用一堆又臭又长的标签去干 Java 一行代码搞定的事,就叫高级了?

SimpleDAO 的做法:

@Setter @Getter
public class OrderCond extends BaseCondition {
    private String name;
    private Integer age;
    private Integer[] inAges;
    private Boolean subQuery;
    private String orderNo;
    private String[] orderNos;
    private Byte orderStatus;

    @Override
    protected void addCondition() {
        // 关联表:无语法糖,自己写别名
        add("AND o.order_status = ?", orderStatus);
        add("AND o.order_no LIKE ?", orderNo, 3);
        add("AND o.order_no IN ", orderNos);
        // IN 条件(主表同样适用)
        add("AND t.age IN ", inAges);
        // 静态子查询
        add("AND t.id IN (SELECT user_id FROM bus_order WHERE dr=0)");
        // 动态子查询
        add("AND t.id IN (SELECT user_id FROM bus_order WHERE dr=0)", subQuery);
    }
}

2.2 缺陷二:Mapper 接口 — 为了接口而接口

接口的价值在于多态和抽象。但 MyBatis 的 Mapper 接口:

  • 永远只对应一个 XML/注解实现
  • 方法签名和 SQL 是 1:1 硬绑定
  • 增加的是命名空间匹配、方法名同步、IDE 跳转断裂的成本

这不是"面向接口编程",是框架实现细节的外溢

SimpleDAO 的做法: 直接继承 BaseDao<T>,零代码,无伪抽象。

2.3 缺陷三:SQL 编写白盒,SQL 执行黑盒

MyBatis 把 SQL 编写设计成白盒(你能看到 XML),但 SQL 执行设计成黑盒(拦截器、代理链、缓存层)。

想加个数据权限?写拦截器。想加个脱敏?写拦截器。那拦截器是普通人能搞定的吗?80% 的程序员写 20 行代码,连业务的边都摸不着,全在跟框架内部对象搏斗。

SimpleDAO 的做法: 全是白盒。AOP 直接拦截参数和返回值,10 分钟搞定。

2.4 缺陷四:补坑生态 — 受害者联盟

“传统框架生态繁荣?那是补坑生态!”

为什么 Spring JDBC 没人讨论?因为它没坑,白盒到底。传统框架呢?SQL 编写白盒,SQL 执行黑盒。它自己制造了坑,然后让开发者写插件来填坑。你在为框架的设计缺陷买单。

SimpleDAO 的做法: 无坑,无需生态。

2.5 缺陷五:单表思维 — 背离 RDBMS 的本质

RDBMS,第一个单词就是 Relation(关系)。企业级开发中,单表场景占比极低,越庞大的系统比例越小。MyBatis-Plus 所谓的"单表解决 80% 业务",是行业最大的谎言之一

真实的企业开发,核心是多表联动。MyBatis-Plus 的单表 Wrapper,到了联表场景完全失能,只能退回 XML。

SimpleDAO 的做法: 联表是常态,单表是特例。BaseDao 只是 BaseSql 的预配置,底层完全统一。

2.6 缺陷六:31 类自造异常 — 中间商制造错误

Spring JDBC 只有 2-3 类异常。MyBatis 有 31 类。你花时间学的不是"数据库出了什么问题",而是"框架的哪一层又炸了"。

SimpleDAO 的做法: 基于 Spring JDBC,异常就是数据库真实的异常,没有中间商赚差价,更没有中间商制造错误。

2.7 缺陷七:七层执行链路 — 性能损耗

业界公认的性能排名:原生 JDBC 100%,Spring JDBC 99%,MyBatis 95%。那 5% 差在哪?就差在那多出来的四五层抽象、动态代理、反射调用上。

SimpleDAO 的做法: 三层到底,直通数据库,性能 ≈ Spring JDBC。


三、SimpleDAO 的设计哲学

SimpleDAO 不是"另一个 ORM",是对 ORM 范式的根本否定。它的设计哲学可以概括为一句话:

面向业务设计,而非面向数据库设计。

3.1 核心抽象:SQL 结构的动态梯度

SimpleDAO 对 SQL 结构做了关键抽象:不同位置的动态程度不同

SQL 位置 动态程度 SimpleDAO 设计
WHERE 高度动态 BaseCondition — 核心抽象
分页 (LIMIT/OFFSET) 高度动态 page() / page0() — 自动处理
JOIN / ON 中度动态 BaseCondition 嵌入任意位置
ORDER BY 中度动态 BaseCondition.orders
GROUP BY / HAVING 中度动态 BaseCondition 嵌入 HAVING
SELECT 字段 高度稳定 手写 SQL,框架不干预
FROM 表名 高度稳定 手写 SQL,框架不干预

三个主类的分工完全对应这个梯度:

  • BaseCondition — 负责"高度动态"部分
  • BaseSql — 负责"中度动态"部分(执行、分页)
  • BaseDao — 负责"高度稳定"部分的自动化(单表 CRUD)

好的,这段建议插在"3.1 核心抽象:SQL 结构的动态梯度"之后,作为"3.2 单表对象化:零代码 CRUD":


3.2 单表对象化:零代码 CRUD

ORM 的理想是"单表对象化"——把数据库表映射成 Java 对象,CRUD 操作像操作对象一样自然。JPA/Hibernate 为此付出了沉重的代价(复杂的实体状态管理、HQL、缓存、N+1 问题),而 MyBatis 连这个理想都没真正实现——它的单表 CRUD 仍然需要写 Mapper 接口和 XML/注解。

SimpleDAO 实现了真正的单表对象化,而且代价趋近于零:

@Repository
public class UserDao extends BaseDao<User> {
    // 空类,获得全部 CRUD 能力
}

一个空类,零代码,获得:

  • save(User) — 自动填充审计字段、雪花主键、逻辑删除标记
  • update(User) — 非空字段更新,自动填充 updateTime/updateBy
  • delete(id...) — 自动判断逻辑删除或物理删除
  • findById(id) — 按主键查询
  • list(cond) — 按条件查询列表
  • page(cond) — 按条件分页查询

这不是"代码生成器生成的模板代码",是框架运行时自动推断

  • @Table("sys_user") → 表名
  • @Id → 主键名和类型
  • 字段名(驼峰)→ 自动转下划线
  • createTime/createBy/updateTime/updateBy → 自动审计
  • dr → 自动逻辑删除

单表是联表的特例,不是独立的世界

BaseDao 的底层就是 BaseSql

public <C extends BaseCondition> Page<T> page(boolean show, final C c) {
    String sql = Sql.builder().select().fields(fields).from().table(table).as().sql();
    return page(show, sql, c, clazz);
}

自动生成单表 SQL,然后调用 BaseSql.page()。单表和联表共享同一套执行机制,没有"单表用一套 API、联表退回另一套"的认知割裂。

零代码不是魔法,是元数据缓存

启动时反射解析一次实体类的 @Table@Id、字段列表,缓存到 BaseDao 实例中。运行时零反射,性能无损。

这是"单表对象化"的正确实现方式:编译期零侵入(只有注解),运行期零反射(启动时缓存),开发期零代码(空类继承)


3.3 四象限正交分解:关系代数的必然

任何查询结果都是二维表(行集合 × 列集合),必定属于四象限之一:

单列 多列
单行 field() — 聚合函数 row() — 报表行
多行 columns() — ID 列表 list() — 实体列表

4 × 2 × 2 = 16 个方法,但只需要记 4 个概念(行 × 列),日志开关和参数形式是上下文自然选择。

这是数学意义上的完备性——笛卡尔积覆盖所有组合,无遗漏、无冗余。


3.4 条件类:极简的动态条件构造

SimpleDAO 屏蔽了占比 90%+ 的第一层 if

传统框架(MyBatis XML、JPA Criteria、MyBatis-Plus Wrapper)中,动态条件的核心痛点不是"拼字符串",是无处不在的 if 判断

// MyBatis XML
<if test="name != null and name != ''">
    AND name LIKE CONCAT('%', #{name}, '%')
</if>

// JPA Criteria
if (name != null && !name.isEmpty()) {
    predicates.add(cb.like(root.get("name"), "%" + name + "%"));
}

// MyBatis-Plus Wrapper
if (StringUtils.isNotBlank(name)) {
    wrapper.like("name", name);
}

每一个条件字段,都需要显式写一层 if。10 个条件字段,就是 10 层 if。这是模板代码的瘟疫

SimpleDAO 的 add() 方法内部做了空值判断:

protected final void add(final String sql, final Object value) {
    if (Objects.nonNull(value) && StringUtils.hasText(value.toString())) {
        condition.append(BLANK).append(sql);
        paramList.add(value);
    }
}

开发者只需要写:

add("AND t.name LIKE ?", name, 3);     // 空值自动跳过
add("AND t.age >= ?", ageMin);          // 空值自动跳过
add("AND t.status = ?", status);        // 空值自动跳过

不需要写 if

这个设计的收益是系统性的:

  • 代码量减少 60% — 10 个条件从 30 行 XML/Wrapper 变成 10 行 add()
  • 认知负担归零 — 不需要判断"这个字段要不要加 if",框架已经处理了
  • Null 安全 — 不会因为忘记判空而导致 SQL 语法错误
  • 意图清晰 — 每行代码只说"这个条件是什么",不说"这个条件要不要加"

这 90%+ 的 if 不是"被优化掉了",是被框架吸收了。开发者只关注那 10% 真正需要业务逻辑控制的动态条件,其余全部交给框架的默认行为。

极简不是"功能少",是"需要决策的点少"。

3.5 条件类:不区分单表/联表,位置自由

这是 SimpleDAO 的独特设计,业界没有先例:

// 同一个条件类,既能用于单表,也能用于联表
// 既能用在 WHERE 后,也能用在 ON 后、HAVING 后、甚至子查询里

// WHERE 后
"SELECT ... FROM a WHERE 1=1" + cond.where()

// ON 后
"SELECT ... FROM a JOIN b ON a.id = b.a_id " + cond.and()

// 子查询里
"SELECT ... FROM a WHERE id IN (SELECT id FROM b WHERE " + cond.where() + ")"

// HAVING 后
"SELECT ... GROUP BY ... HAVING 1=1 " + cond.and()

3.6 真动态条件:运行时构造,不限于 WHERE

MyBatis 的"动态 SQL"是静态模板 + 条件分支选择。SimpleDAO 的 addDynamic()运行时完全动态生成 SQL 片段

// 数据权限 AOP 切面
@Before("@annotation(auth)")
public void beforeQuery(JoinPoint point, DataAuth auth) {
    BaseCondition cond = (BaseCondition) point.getArgs()[0];
    String userId = request.getHeader(Const.USER_ID);
    cond.addDynamic(" AND " + auth.userField() + " IN", new Object[] { 0, userId });
}

字段名和值都是运行时确定的,这是 MyBatis XML 模板做不到的真动态

3.7 API 业务语义化:意图即代码

方法 业务语义
list() 查列表
page() 分页查
row() 查单行
field() 查单个值
count() 查数量
exists() 查存在性

不是"技术操作",是业务动词。参数和返回值都是业务对象,不是框架对象。

3.8 上下文精准命名:克制即优雅

不追求长方法名自包含,而是类名 + 方法名 + 泛型 + 返回值组合出完整语义

// UserDao.list(UserCond) → "查用户列表"
// OrderDao.page(SQL, OrderCond, OrderVO.class) → "查订单分页"
// ReportDao.field(SQL, ReportCond, Integer.class) → "查报表数值"

命名克制,字符数少,语义同样精确。

3.9 性能白盒:优化不触发 API 变化

大宽表只取必要字段:

// ❌ 不要:SELECT *
userDao.list(cond);

// ✅ 应该:只取需要的字段
String sql = "SELECT id, name FROM user";
baseSql.list(sql, cond, UserMiniVo.class);

同一个 list 方法,SQL 字符串决定性能边界。不需要换 API,不需要学新方法。


四、SimpleDAO 与 Spring JDBC 的关系

4.1 “Spring Jdbc Ultra” 的定位

SimpleDAO 不是"又一个框架",是 Spring JDBC 的原生功能延伸与增强

  • 连接池?Spring 的
  • 事务?Spring 的 @Transactional
  • 参数化查询?JdbcTemplate / NamedParameterJdbcTemplate
  • 结果映射?BeanPropertyRowMapper
  • 批量操作?NamedParameterJdbcTemplate.batchUpdate
  • AOP 扩展?Spring 的标准切面

SimpleDAO 自己做的只有三件事:

  1. 启动时反射解析元数据(@Table@Id、字段映射)并缓存
  2. 条件拼接(BaseCondition 的字符串操作)
  3. SQL 组装(BaseDao 的单表 SQL 生成)

4.2 全是收益,零成本

站在 Spring JDBC 的基准线上,SimpleDAO 的每一个增强都是纯收益:

Spring JDBC 原生 SimpleDAO 增强 收益
手写 INSERT/UPDATE/DELETE save(t) / update(t) / delete(id) 零代码单表 CRUD
手动处理 null 字段 update() / updateNull() 区分 语义明确
手写 WHERE 条件 BaseCondition.add() 类型安全,参数化
手写 COUNT + LIMIT page() 一行代码,智能 COUNT
手动设置审计字段 自动填充 不侵入业务
手动处理逻辑删除 delete() 自动判断 统一策略

4.3 能力上限 = SQL 的上限 = Spring 的上限

  • SQL 能力无上限:窗口函数、CTE、递归查询、存储过程……只要数据库支持,SimpleDAO 就能执行
  • Spring 生态扩展无上限:事务、多数据源、缓存、AOP、监控……100% 原生
  • 性能 ≈ Spring JDBC:启动时反射一次并缓存,运行时零反射,联表完全不读元数据

五、与 MyBatis 的终极对比

维度 MyBatis SimpleDAO
设计哲学 面向数据库 面向业务
SQL 控制权 半遮半掩(XML 白盒,执行黑盒) 完全白盒
动态 SQL XML 标签 + OGNL Java 原生 if
联表查询 标签地狱,SQL 切碎 完整 SQL 直写
结果映射 resultMap,写两遍 BeanPropertyRowMapper,零配置
扩展机制 拦截器,高门槛 AOP,零门槛
学习成本 数周到数月(含踩坑) 2 小时(8 个案例)
代码量 基准 1/3 ~ 1/4
性能 95% 99%
异常体系 31 类自造异常 仅数据库真实异常
执行链路 七层 三层
生态本质 受害者联盟,补坑生态 无坑,无需生态

六、结语:把时间留给生活,而不是框架

SimpleDAO 不是为了成为又一个流行的框架,而是为了证明一件事:

技术可以更简单,开发可以更愉快,程序员可以早下班。

如果你:

  • 厌倦了复杂框架的折磨
  • 想要高效完成工作
  • 想要早点回家陪家人
  • 相信简单比复杂更有力量

那么,SimpleDAO 为你而存在。

SimpleDAO: SQL-First,白盒透明,能力无上限。


本文基于 SimpleDAO 1.2.1 版本及 8 个企业实战案例撰写。框架已在生产环境稳定运行 3 年+,支撑日均百万级请求,服务十余家企业客户。


相关开源地址

  1. 核心框架源码:https://gitee.com/gao_zhenzhong/simple-dao
  2. 系统底座:https://gitee.com/gao_zhenzhong/simple-dao-starter
  3. 代码生成器:https://gitee.com/gao_zhenzhong/simple-dao-coder
  4. 实战案例(本集源码):https://gitee.com/gao_zhenzhong/simple-dao-demo
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐