一直对 MyBatis 有个刻板印象: Mapper 接口负责声明方法,Mapper.xml 负责写 SQL。
改条件就去 XML 里
<if test="">,调参数就切换不同文件,从刚开始学到现在用了很久,熟悉得不能再熟悉。
直到最近看到一个项目:
我把
resources/mapper 翻了个底朝天,愣是没找到一份 XML。
我第一反应:
“这项目肯定是全用
@Select之类的注解硬写 SQL 了吧。”
结果打开 Mapper:我人傻了。
单表按主键查,注解很舒服:
@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);
这么些的体验基本是:
所以绝大多数团队最终都会回到 XML——至少 XML 里写动态 SQL 还能接受。
这个项目里 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。
这类写法的本质是:
动态条件 + 可读性。
不用在注解字符串里写
<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 低。
我的建议:
List
list(UserQuery req);
Provider 里直接
#{name}、
#{minAge},对应
req 的属性名即可。
@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,明明传了值怎么为空”。
如果主要场景是 单表 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(XML/Provider/自定义 mapper 都行),Wrapper 不适合硬扛报表类需求。
如果连 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”再理解意图。嵌套层级越深,这个成本越高。
参考落地策略:
@Select 足够@SelectProvider + SQL BuilderProvider 这条路最让我意外: 不靠第三方,也不把动态 SQL 写成字符串炼狱,但它也不是用来替代所有 SQL 的。把边界定好,用起来会更舒服。
总之就是在不同场景下面选择合适的技术并确定合理的规范,然后统一按照规范执行就可以啦!
<"https://dcnh5chji6j1.feishu.cn/docx/JOhqdNJDqok5n4xElkTcG8FSnue"><"https://dcnh5chji6j1.feishu.cn/docx/TVZcdtCY1o6qDhxGpyDcjY1Wn4c">
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/53956/viewspace-3115101/ ,如需转载,请注明出处,否则将追究法律责任。