ITPub博客

首页 > 应用开发 > Javascript > 写了 10 年 MyBatis,一直以为“去 XML”=写注解,直到看到了这个项目

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

Javascript 作者:oliseh 时间:2026-04-01 10:39:53 0 删除 编辑

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

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

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

我第一反应:

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

结果打开 Mapper:我人傻了。

1)去 XML:@Select

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


@Select(

"select * from tb_user where id = #{id}")UserDO selectById(
Long 
id);

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


@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 长这样:


@SelectProvider(

type = UserSqlProvider.
class, method = 
"selectByCondition")List selectByCondition(
UserQuery req);

我当时心里一句话:

“Provider?这是什么东东?”

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

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 之间跳来跳去。

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


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 单参数对象:最舒服

List 
list(UserQuery req);

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

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


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

Provider 可以收 Map:


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 来表达条件。


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 确实很直观:

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 平均订单金额”这种:

// 子查询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 的。把边界定好,用起来会更舒服。

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

<"https://dcnh5chji6j1.feishu.cn/docx/JOhqdNJDqok5n4xElkTcG8FSnue"><"https://dcnh5chji6j1.feishu.cn/docx/TVZcdtCY1o6qDhxGpyDcjY1Wn4c">

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/53956/viewspace-3115101/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
不仅仅专注Oracle database技术, member of SHOUG

注册时间:2014-04-06

  • 博文量
    300
  • 访问量
    1959625