一直对 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/param2 或 arg0/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 的。把边界定好,用起来会更舒服。
总之就是在不同场景下面选择合适的技术并确定合理的规范,然后统一按照规范执行就可以啦!