我为什么把 MyBatis 从项目中删了?

一个基于 Spring JDBC 的持久层方案,让代码量减少 60%

一、先给你看三个对比------如果你写过 MyBatis,你会懂

对比一:动态条件拼接

场景: 根据用户名模糊查询、年龄区间、部门ID、ID集合的 IN 查询。

MyBatis 写法(XML):

xml 复制代码
<if test="name != null and name != ''">
    AND user_name LIKE concat('%', #{name}, '%')
</if>
<if test="ageMin != null">
    AND age >= #{ageMin}
</if>
<if test="ageMax != null">
    AND age <= #{ageMax}
</if>
<if test="deptId != null">
    AND dept_id = #{deptId}
</if>
<if test="ids != null and ids.size > 0">
    AND user_id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</if>

~15 行,每个条件写两遍(if 判断 + SQL 片段),而且你必须保证参数名、字段名、SQL 片段三者一致,改一个地方要同步改三个位置。

SimpleDAO 写法(Java,BaseCondition 子类):

java 复制代码
@Override
protected void addCondition() {
    and("name LIKE", name, 3);      // 一行,自动处理空值
    and("age >=", ageMin);           // 一行
    and("age <=", ageMax);           // 一行
    and("dept_id =", deptId);        // 一行
    in("user_id", ids);              // 一行,自动展开数组
}

5 行,每个条件一行,没有冗余判断,没有标签闭合,没有参数名重复声明。

对比二:IN 条件

场景: 按一组 ID 查询用户。

MyBatis 写法:

xml 复制代码
<if test="ids != null and ids.size() > 0">
    AND user_id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</if>

6 行(if 开始、SQL 片段、foreach 开始、循环体、foreach 结束、if 结束)

SimpleDAO 写法:

java 复制代码
in("user_id", ids);

1 行

对比三:子查询 + 关联表条件 + 逻辑开关

场景: 查询订单,条件包括订单状态、金额下限、用户姓名、用户手机号,并且根据业务开关决定是否只查"有有效订单"的用户。

SimpleDAO 写法(OrderCond):

java 复制代码
@Override
protected void addCondition() {
    // 主表条件
    and("order_status =", status);
    and("total_amount >=", amountMin);

    // 关联表条件(用户表)------直接写 SQL 片段
    add("AND u.name LIKE ?", userName, 3);
    add("AND u.phone = ?", userPhone);

    // 动态子查询------一行搞定,逻辑开关控制
    add("AND u.id IN (SELECT user_id FROM bus_order WHERE dr = 0)", subQueryEnabled);
}

关键点: add 方法不区分"主表条件"、"关联表条件"、"子查询条件"。它只做两件事:拼接字符串、收集参数。你写什么 SQL 片段,它就拼什么,它不解析、不拦截、不转换。

同样的 add 方法,既能拼主表条件,也能拼关联表条件,还能拼子查询------因为我们的条件类不关心 SQL 片段长什么样,它只关心两件事:这个片段要不要拼进去、对应的参数放哪个位置。

二、设计哲学:为什么可以这样写

2.1 起点:80 行代码解决核心痛点

这套体系的起点,是一个 80 行的 Sql 工具类。核心思想极其朴素:

动态条件拼接 = 字符串拼接 + 参数收集

你只需要一个 StringBuilder 存 SQL 片段,一个 List<Object> 存参数值。每次 add 方法同时操作这两个结构,确保"片段"和"参数"永远一一对应。

这个工具类不需要 XML,不需要 OGNL,不需要 Mapper 接口。它只是一个纯 Java 的"拼接器"。但它解决的是 MyBatis 用户每天最痛苦的事------动态条件拼接。

2.2 三层架构:各司其职,互不侵入

层级 职责 核心类
执行层 发 SQL、收结果 BaseSql
对象化层 单表 CRUD、实体映射 BaseDao
拼接层 动态条件、参数收集 BaseCondition

关键设计: BaseCondition 既是 SQL 片段的拼接器,也是参数的收集器。add 方法同时干了这两件事,所以永远不会出现"条件跟参数对不上"的问题。

BaseSql 不知道什么是实体、什么是条件------它只负责把 SQL 和参数发出去。BaseDao 不知道什么是联表、什么是子查询------它只负责管理单表的 CRUD。BaseCondition 不知道什么是事务、什么是数据源------它只管拼字符串和收参数。

三层之间通过"参数数组"和"SQL 字符串"连接,没有任何一层依赖另一层的内部状态。

2.3 双不封装:保持对 SQL 的完全控制力

不封装关键字和运算符

我们不提供 .eq().gt().like() 这类链式 API。你直接写 AND age >= ?

原因: SQL 语法是稳定的、完整的、经过数十年验证的,不需要 Java 再封装一层。

代价: 封装等于穷举,穷举必然遗漏。每当遇到 BETWEENREGEXPMATCH AGAINSTJSON_CONTAINS,封装好的 API 就会露出缺口,开发者被迫退回到"拼字符串"。那为什么不从一开始就保持"拼字符串"的权利?

不封装 Spring JDBC 异常

所有异常都来自 JdbcTemplate,只有 DataAccessException 这一套体系。

原因: Spring 的异常体系已经足够完善,不需要再包装一层 PersistenceException

代价: 你不需要学第二套异常类型,不需要在 MyBatis 的 31 类异常和 Spring 的异常之间来回翻译。

"双不封装"带来的副作用: BaseCondition 不区分单表、联表、子查询、半连接、SQL 片段。因为它只做最纯的"拼接 + 参数收集",它根本不管前面的 SQL 是什么结构。

2.4 双重复用:一次封装,处处可用

BaseCondition 在 BaseDao 和 BaseSql 两层复用
  • BaseDao 用条件类生成单表的 WHERESET
  • BaseSql 用条件类对联表、报表、子查询生成 WHERE
  • 同一个条件类,单表联表都能用
BaseDao 复用 BaseSql 的执行能力
  • BaseDao 只负责"生成 SQL 的静态部分"(SELECT、INSERT、UPDATE、DELETE)
  • BaseSql 只负责"把 SQL 和参数发出去"
  • 两者通过条件类连接,互不依赖

2.5 五大独创

独创 1:单表、联表同一套 API 族

用户从单表过渡到联表时,心智负担为零。你只需要改 SQL 字符串,其他不变。

独创 2:单表、联表同一套条件类

BaseCondition 不区分单表、联表、子查询、半连接、SQL 片段。因为它只做最纯的"拼接 + 参数收集"。

独创 3:屏蔽条件中的第一层 if

add 方法内部自动判断 value != null && value != ""。开发者不用写 if(userName != null),直接写条件。减少 90% 的样板代码。

独创 4:参数合并(mergeParams

多个条件类各自独立构造,最后一行合并参数数组。解决报表/复杂查询场景下的参数顺序耦合问题。

独创 5:正交完备的 API(基于关系代数)

四个核心方法:field(单行单列)、row(单行多列)、columns(多行单列)、list(多行多列)。

基于关系代数的闭包性质设计------两个关系的运算结果还是一个关系,在 Java 里表现为这四个象限。不是拍脑袋穷举出来的。

三、实战案例:哲学不是空谈

案例一:单表 CRUD(零代码继承)

java 复制代码
@Data
@Table("sys_user")
public class User {
    @Id
    private Long id;
    private String name;
    private Integer age;
    // 审计字段自动填充:createTime、createBy、updateTime、updateBy
    private Byte dr;  // 逻辑删除,自动处理
}

@Repository
public class UserDao extends BaseDao<User> {
    // 空类即可获得:save、update、delete、list、page、findById 等全部 CRUD 能力
}

DAO 层零代码。 继承空类就能获得完整 CRUD 能力。

案例二:联表查询(同样的 API)

java 复制代码
@Repository
public class OrderDao extends BaseDao<Order> {
    private final static String SQL = """
        SELECT t.*, u.name user_name, u.phone user_phone
        FROM bus_order t
        LEFT JOIN sys_user u ON t.user_id = u.id
        """;

    public Page<OrderVO> pageJoin(OrderCond cond) {
        return page(SQL, cond, OrderVO.class);  // 同样的 page 方法
    }
}

从单表到联表,只是改了一个 SQL 字符串,其他代码完全不变。

案例三:mergeParams------多条件合并

java 复制代码
TimeCond timeCond = TimeCond.builder().orderTimeStart(start).orderTimeEnd(end).build();
BizCond bizCond = BizCond.builder().goodsName("手机").priceMin(1000.0).build();
ValidCond validCond = ValidCond.builder().userName("张").build();

String sql = """
    SELECT t.goods_name, COUNT(o.id) order_count
    FROM t_goods t
    JOIN (SELECT id, user_id FROM t_order WHERE dr=0 """ + timeCond.and() + """) o
        ON t.order_id = o.id
    WHERE t.dr = 0 """ + bizCond.and() + """
      AND o.user_id IN (SELECT id FROM t_user """ + validCond.where() + """)
    GROUP BY t.goods_name
    """;

// 一行合并所有参数,按 SQL 中 ? 出现的顺序精准匹配
list(sql, ReportVo.class, mergeParams(timeCond, bizCond, validCond));

三个条件类各自独立构造,在 SQL 不同位置嵌入,最后一行合并参数。

案例四:数据权限------AOP + extendCondition

java 复制代码
@Aspect
@Component
public class DataAuthAspect {
    @Before("@annotation(auth)")
    public void beforeQuery(JoinPoint point, DataAuth auth) {
        BaseCondition cond = (BaseCondition) point.getArgs()[0];
        String userId = request.getHeader("X-User-Id");
        // 一行注入权限条件
        cond.setExtendCondition(" AND " + auth.userField() + " IN (" + userId + ",0)");
    }
}

@Repository
public class NoticeDao extends BaseDao<Notice> {
    @DataAuth(userField = "t.receiver")
    public Page<NoticeVO> pageJoin(NoticeCond cond) {
        return page(SQL, cond, NoticeVO.class);
    }
}

MyBatis 用户要写 Interceptor 去扒 BoundSql,风险极高。你只需要 setExtendCondition,像喝水一样简单。

四、与 MyBatis 的核心对比

维度 MyBatis SimpleDAO
动态条件 XML 标签,一行变三行 add 方法,一行解决
IN 条件 6 行(if + foreach + 闭合) 1 行
关联表条件 需在 XML 中混写 同一套 add API
子查询 XML 标签嵌套,难以维护 add("AND ... IN (SELECT ...)", flag) 一行
数据权限 拦截器扒 BoundSql,风险高 AOP + setExtendCondition
脱敏 写 TypeHandler 或手动 AOP 代理 Service 方法
多条件合并 参数顺序在 XML 中耦合 mergeParams 一行合并
异常体系 31 类自造异常 DataAccessException
学习成本 XML + OGNL + 标签 + 拦截器 懂 SQL + Spring JDBC 即可

五、框架与工具链

核心框架

https://gitee.com/gao_zhenzhong/simple-dao

核心就三个类:BaseDaoBaseSqlBaseCondition,总共 ~300 行核心逻辑。没有任何黑盒拦截器,没有任何 XML 解析开销,没有任何 OGNL 表达式引擎。

管理系统完整底座(RBAC 权限、鉴权、前后端脚手架)

https://gitee.com/gao_zhenzhong/simple-dao-starter

包含完整的用户、角色、权限、菜单管理,前后端分离脚手架。开箱即用,基于 SimpleDAO 构建,所有数据访问层天然享受 SimpleDAO 的一切优势。

代码生成器

https://gitee.com/gao_zhenzhong/simple-dao-coder

一键生成 Entity、Cond、Dao、Service、Controller、VO、前端页面模板(Vue + Element)。生成的代码是标准 Java,不是黑盒魔法,你可以自由修改、Code Review、对接任意技术栈。

实战案例仓库

https://gitee.com/gao_zhenzhong/simple-dao-demo

包含 8 个可运行案例:单表 CRUD、联表分页、子查询、报表聚合、多条件合并、数据权限、脱敏。内置 H2 内存数据库,开箱即跑。

六、总结

SimpleDAO 不造新轮子,它只是把 Spring JDBC 的潜力释放到了极致------让你用最少的代码、最简的心智、最透明的 SQL,完成企业级开发。

我们为自己写的,是一套能让自己少加班、早回家的工具。如果你也受够了 MyBatis 的 XML 和 OGNL,欢迎来试试。

能力上限 = SQL 表达的上限 = Spring 生态的上限。没有任何人为设置的边界。

  • 代码量: 减少 60%-80%
  • 学习成本: 会 SQL +Java 即可
  • 调试体验: 复制 SQL 到 Navicat 就能跑
  • 性能: ≈ Spring JDBC,无中间层损耗
  • 迁移成本: 零------可与 MyBatis 无缝共存

把时间留给生活,而不是框架。

相关推荐
公爵爱学习1 小时前
无人机多点导航笔记
java·前端·笔记
爱折腾的编程老炮1 小时前
潮玩品牌自建抽盒机小程序——商城 + 抽盒 + 一番赏 + 会员 + 社区,一套代码怎么搭
spring boot·小程序·vue·mybatis·uniapp
萧瑟余晖2 小时前
Java深入解析篇五十五之Unsafe 机制详解
java
kakawzw2 小时前
mybatis源码笔记1——JDBC和整体架构
java·mybatis
sunshine22 girl2 小时前
Java学习一 环境配置1 安装JDK,配置环境变量
java·开发语言·学习
秋饼2 小时前
Spring AI Session API 深度实战:从 ChatMemory 平滑迁移到事件溯源的企业级短期记忆
java·ai·技术分享·后端开发
QCodingDev2 小时前
Spring AI 2.0企业级RAG实战:引用校验、无依据拒答与知识治理怎么做?
java·人工智能·spring·ai
吴声子夜歌2 小时前
ApacheCommons——commons-math3(科学计算与线性代数)(一)
java·线性代数·算法·apache
砚底藏山河2 小时前
行情工程实战 M01|存储选型 CSVSQLiteMySQL
java·python·金融·maven