01o00o10

MyBatis-Plus 源码阅读(七):Wrapper 怎么把 Lambda 变成 SQL

MyBatis-Plus 源码阅读(七):Wrapper 怎么把 Lambda 变成 SQL

下面这段代码看起来像在写 Java,最后却会变成 SQL:

Wrappers.<User>lambdaQuery()
    .eq(User::getName, "Alice")
    .ge(User::getAge, 18)
    .orderByDesc(User::getId);

Wrapper 主要解决两件事:把条件按正确顺序组织起来,以及把参数变成安全的占位符。

AbstractWrapper 的三个泛型

类声明看着有点吓人:

AbstractWrapper<T, R, Children>

先这样理解:

  • T:实体类型;
  • R:列的表达方式,字符串或 Lambda;
  • Children:链式调用返回的具体 Wrapper 类型。

这里用到了 CRTP(奇异递归模板模式),Children 泛型的作用是确保链式调用 eq()ne()orderByAsc() 等方法时始终返回正确的子类类型,而不是退化为父类。

QueryWrapper<User>RString,所以写 "age"LambdaQueryWrapper<User>RSFunction<User, ?>,所以写 User::getAge

底层拼条件的逻辑大部分都在 AbstractWrapper,两种 Wrapper 主要差在“列名怎么得到”。

eq 到底做了什么

eq() 最后进入 addCondition()

eq(column, value)
    -> addCondition(condition, column, sqlKeyword, val)
    -> doIt(condition, () -> columnToString(column), sqlKeyword, () -> formatSql("{0}", val))
    -> formatParam()
    -> appendSqlSegments()

formatSql("{0}", val) 会调用 formatParam() 来生成参数名。formatParam() 不会把值直接拼到 SQL 中,它会生成参数名:

MPGENVAL1
MPGENVAL2
...

并把值放进 paramNameValuePairs

最终片段大概是:

age >= #{ew.paramNameValuePairs.MPGENVAL1}

这样参数仍由 MyBatis 绑定,不需要把用户输入直接拼进 SQL。

条件片段放在哪里

appendSqlSegments() 会把片段交给 MergeSegments

MergeSegments 按类型保存:

普通条件 -> NormalSegmentList
GROUP BY -> GroupBySegmentList
HAVING   -> HavingSegmentList
ORDER BY -> OrderBySegmentList

源码中 MergeSegmentsadd() 方法会根据第一个片段的类型,将其分派到对应的 SegmentList 中。

普通条件列表还会处理多余的 AND、OR 和嵌套括号。

所以这段:

wrapper.eq("a", 1)
       .or()
       .and(w -> w.eq("b", 2).eq("c", 3));

不是简单字符串相加。片段先进入列表,最后由各 SegmentList 输出结构正确的 SQL。

Lambda 怎么知道字段名

User::getAge 的类型是可序列化函数 SFunction

AbstractLambdaWrapper.getColumnCache() 会调用:

LambdaUtils.extract(column);

LambdaUtils.extract() 内部会依次尝试三种解析策略:

  1. IDE 调试代理处理:如果 funcProxy 实例(IDEA 调试模式下 Lambda 表现为代理),使用 IdeaProxyLambdaMeta 解析;
  2. 反射读取 writeReplace:通过反射调用 Lambda 的 writeReplace() 方法,拿到 SerializedLambda,封装为 ReflectLambdaMeta
  3. 序列化兜底:如果反射失败,使用 ShadowLambdaMeta 通过序列化方式提取。

拿到 LambdaMeta 后,可以获取实现方法名:

getAge

再通过 MyBatis 的 PropertyNamer.methodToProperty() 转成:

age

PropertyNamer.methodToProperty() 的逻辑是:如果方法名以 is 开头,去掉 is 前缀;如果以 getset 开头,去掉 get/set 前缀;然后将首字母小写,最终得到属性名。

最后用这个属性名去 LambdaUtils 的列缓存中查 ColumnCache。列缓存来自 TableInfo,因此能得到真正的数据库列名,而不只是 Java 属性名。

User::getAge
    -> SerializedLambda
    -> getAge
    -> age
    -> TableInfo 字段缓存
    -> age_column

为什么要缓存

解析 Lambda 和查找实体字段都涉及反射。LambdaUtils 会缓存实体的列映射,当前源码使用 Map<String, Map<String, ColumnCache>> 结构,key 是实体类全限定名,value 是属性名到 ColumnCache 的映射。

LambdaUtils 还使用 ClassValue 来缓存 Lambda 类的 writeReplace 查找结果,避免每次调用都重新反射。

这也是为什么正常使用时不必担心每个 eq(User::getAge, 18) 都完整反射一次实体。

condition 参数很有用

Wrapper 的多数方法都有 condition 重载:

wrapper.eq(name != null, User::getName, name);

内部通过 maybeDo() 决定是否添加片段。maybeDo() 的核心逻辑是:如果 conditiontrue,则执行传入的 Supplier,否则直接返回当前实例。无 condition 参数的重载版本内部默认传入 condition = true,条件必然生效。

相比在外面写很多 if,这种写法能保持条件集中。不过条件过多时,先在业务代码里整理输入,通常比造一条很长的链更容易维护。

几个需要小心的方法

apply()last()、部分字符串列名 API 给了调用者更大的自由,也意味着更大的责任。

参数值优先使用占位写法:

wrapper.apply(
    "date_format(create_time, '%Y-%m-%d') = {0}",
    day
);

不要把用户输入直接拼进 last() 或列名。Wrapper 能保证自己管理的值参数化,但无法替你判断一段原始 SQL 字符串是否安全。

Wrapper 本身还有可变的参数序号和片段列表,也不适合跨线程复用。

动手看内部结果

LambdaQueryWrapper<User> wrapper =
    Wrappers.<User>lambdaQuery()
        .eq(User::getName, "Alice")
        .ge(User::getAge, 18);

System.out.println(wrapper.getSqlSegment());
System.out.println(
    wrapper.getParamNameValuePairs()
);

推荐断点:

  1. AbstractWrapper.eq()
  2. AbstractWrapper.addCondition()
  3. AbstractWrapper.formatParam()
  4. MergeSegments.add()
  5. AbstractLambdaWrapper.getColumnCache()
  6. LambdaUtils.extract()
  7. PropertyNamer.methodToProperty()

posted on 2026-09-22 09:26  01o00o10  阅读(1)  评论(0)    收藏  举报

导航