写了 10 年 MyBatis,一直以为“去 XML”=写注解,直到看到了这个项目

一直对 MyBatis 有个刻板印象:Mapper 接口负责声明方法,Mapper.xml 负责写 SQL

改条件就去 XML 里 <if test="">,调参数就切换不同文件,从刚开始学到现在用了很久,熟悉得不能再熟悉。

直到最近看到一个项目:我把 resources/mapper 翻了个底朝天,愣是没找到一份 XML。

我第一反应:

"这项目肯定是全用 @Select 之类的注解硬写 SQL 了吧。"

结果打开 Mapper:我人傻了。

1)去 XML:@Select

单表按主键查,注解很舒服:

python 复制代码
@Select("select * from tb_user where id = #{id}")UserDO selectById(Long id);

但一旦要动态条件,很多人会写成这种:

less 复制代码
@Select("" +        "select * from tb_user " +        "" +        "   and name like concat('%', #{name}, '%') " +        "   and age >= #{age} " +        "" +        "")List list(UserQuery req);

这么些的体验基本是:

  • • 字符串拼接看得眼疼
  • • 没有 SQL 高亮、格式化也很难受
  • • 复杂一点直接维护灾难

所以绝大多数团队最终都会回到 XML------至少 XML 里写动态 SQL 还能接受。

2)去 XML:@SelectProvider

这个项目里 Mapper 长这样:

python 复制代码
@SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")List selectByCondition(UserQuery req);

我当时心里一句话:

"Provider?这是什么东东?"

点进 UserSqlProvider,看到的是这种代码:

scss 复制代码
public class UserSqlProvider {  public String selectByCondition(UserQuery req) {    return new SQL() {{      SELECT("id, name, age, status, create_time");      FROM("tb_user");      if (req.getName() != null && !req.getName().isBlank()) {        WHERE("name like concat('%', #{name}, '%')");      }      if (req.getMinAge() != null) {        WHERE("age >= #{minAge}");      }      if (req.getStatus() != null) {        WHERE("status = #{status}");      }      ORDER_BY("create_time desc");    }}.toString();  }}

当时我有点惊讶: SQL()SELECT()WHERE() 这些不是自定义工具类,而是 MyBatis 自带的 SQL Builder

这类写法的本质是:

  • XML 动态 SQL 的能力不变
  • • 但"拼 SQL 的载体"从 XML 变成 Java Provider 方法
  • • 最终 MyBatis 仍然执行一段 SQL 字符串(只是这段字符串由 builder 组装出来)

3)Provider

解决了什么?

动态条件 + 可读性

不用在注解字符串里写 <script>、不用手动拼 AND、也不用在 Java/XML 之间跳来跳去。

再比如:动态排序字段(注意做白名单防注入)

vbnet 复制代码
public String list(UserQuery req) {  return new SQL() {{    SELECT("*");    FROM("tb_user");    if (req.getName() != null && !req.getName().isBlank()) {      WHERE("name like concat('%', #{name}, '%')");    }    // 排序字段做白名单,避免 order by 注入    if ("create_time".equals(req.getOrderBy())) {      ORDER_BY("create_time desc");    } else if ("age".equals(req.getOrderBy())) {      ORDER_BY("age desc");    } else {      ORDER_BY("id desc");    }  }}.toString();}

未能解决

复杂 SQL 的"表达力"问题

子查询、复杂 join、窗口函数、CTE......你用 builder 也能写,但写着写着就会变成"在 Java 里造 SQL AST",维护成本可能并不比 XML 低。

我的建议:

  • 中等复杂度动态查询:Provider 很合适
  • 复杂报表 / 多层嵌套:直接写原生 SQL(放 XML 或统一的 SQL 文件)更直观

4)Provider 最容易踩的坑:参数绑定(90% 的报错在这)

4.1 单参数对象:最舒服

scss 复制代码
List list(UserQuery req);

Provider 里直接 #{name}#{minAge},对应 req 的属性名即可。

4.2 多参数一定要 @Param,不然会看到奇怪的参数名

less 复制代码
@SelectProvider(type = UserSqlProvider.class, method = "get")UserDO get(@Param("id") Long id, @Param("status") Integer status);

Provider 可以收 Map

typescript 复制代码
public String get(Map p) {  return new SQL() {{    SELECT("*");    FROM("tb_user");    WHERE("id = #{id}");    if (p.get("status") != null) {      WHERE("status = #{status}");    }  }}.toString();}

如果不写 @Param,参数名可能变成 param1/param2arg0/arg1,然后你就开始"有bug,明明传了值怎么为空"。

5)再懒一下:MyBatis-Plus

如果主要场景是单表 CRUD + 条件筛选,MyBatis-Plus 的思路是:尽量别写 SQL,让 Wrapper 来表达条件。

less 复制代码
LambdaQueryWrapper w = Wrappers.lambdaQuery();w.like(StringUtils.isNotBlank(req.getName()), UserDO::getName, req.getName()) .ge(req.getMinAge() != null, UserDO::getAge, req.getMinAge()) .eq(req.getStatus() != null, UserDO::getStatus, req.getStatus()); List list = userMapper.selectList(w);

这套东西的价值很明确:

  • • 字段引用是方法引用,改字段/重构更安全
  • • 大量单表查询不需要写 SQL
  • • 团队统一风格之后,开发效率很高

但边界也很明确:复杂 SQL 仍然要回到原生 SQL(XML/Provider/自定义 mapper 都行),Wrapper 不适合硬扛报表类需求。

6)组装 SQL:MyBatis-Flex

如果连 join 都不想写 SQL,更希望用 Java 结构来表达,MyBatis-Flex 这类框架会提供更强的 QueryWrapper/Join 能力。

简单 join 确实很直观:

scss 复制代码
QueryWrapper q = QueryWrapper.create()    .select(ACCOUNT.ID, ACCOUNT.USER_NAME, ROLE.ROLE_NAME)    .from(ACCOUNT)    .leftJoin(ROLE).on(ACCOUNT.ROLE_ID.eq(ROLE.ID))    .where(ACCOUNT.AGE.ge(18)); List list = accountMapper.selectListByQueryAs(q, AccountDTO.class);

但当你开始写多层子查询/嵌套条件时,可读性很容易被"对象套对象"拉低。

比如"订单金额 > 用户 1 平均订单金额"这种:

vbnet 复制代码
// 子查询QueryWrapper sub = QueryWrapper.create()    .select(avg(ORDER.TOTAL_PRICE))    .from(ORDER)    .where(ORDER.USER_ID.eq(1)); // 主查询QueryWrapper main = QueryWrapper.create()    .select(ORDER.ALL_COLUMNS)    .from(ORDER)    .where(ORDER.TOTAL_PRICE.gt(sub)); List list = orderMapper.selectListByQuery(main);

能写、也类型更安全,但维护者往往需要在脑子里把它"还原成 SQL"再理解意图。嵌套层级越深,这个成本越高。

7)到底怎么选?

参考落地策略:

  • 固定 SQL / 简单单表@Select 足够
  • 中等动态 SQL(条件多、拼接多,但逻辑清晰)@SelectProvider + SQL Builder
  • 单表 CRUD 为主,追求少写 SQL:MyBatis-Plus
  • Join 多、希望 Java 化表达更强:MyBatis-Flex(嵌套复杂时要克制)
  • 复杂报表 / 多层子查询 / 强声明式:直接原生 SQL(XML/SQL 文件),通常最清晰

Provider 这条路最让我意外:不靠第三方,也不把动态 SQL 写成字符串炼狱,但它也不是用来替代所有 SQL 的。把边界定好,用起来会更舒服。

总之就是在不同场景下面选择合适的技术并确定合理的规范,然后统一按照规范执行就可以啦!

相关推荐
却尘1 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
茶杯梦轩1 小时前
从零起步学习Redis || 第七章:Redis持久化方案的实现及底层原理解析(RDB快照与AOF日志)
redis·后端
QZQ541881 小时前
重构即时IM项目13:优化消息通路(下)
后端
柠檬味拥抱1 小时前
揭秘Cookie操纵:深入解析模拟登录与维持会话技巧
后端
不想打工的码农1 小时前
MyBatis-Plus多数据源实战:被DBA追着改配置后,我肝出这份避坑指南(附动态切换源码)
java·后端
ZeroTaboo1 小时前
rmx:给 Windows 换一个能用的删除
前端·后端
Coder_Boy_2 小时前
Deeplearning4j+ Spring Boot 电商用户复购预测案例
java·人工智能·spring boot·后端·spring
Victory_orsh2 小时前
AI雇佣人类,智能奴役肉体
后端
金牌归来发现妻女流落街头2 小时前
【Springboot基础开发】
java·spring boot·后端