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> 的 R 是 String,所以写 "age";LambdaQueryWrapper<User> 的 R 是 SFunction<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
源码中 MergeSegments 的 add() 方法会根据第一个片段的类型,将其分派到对应的 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() 内部会依次尝试三种解析策略:
- IDE 调试代理处理:如果
func是Proxy实例(IDEA 调试模式下 Lambda 表现为代理),使用IdeaProxyLambdaMeta解析; - 反射读取
writeReplace:通过反射调用 Lambda 的writeReplace()方法,拿到SerializedLambda,封装为ReflectLambdaMeta; - 序列化兜底:如果反射失败,使用
ShadowLambdaMeta通过序列化方式提取。
拿到 LambdaMeta 后,可以获取实现方法名:
getAge
再通过 MyBatis 的 PropertyNamer.methodToProperty() 转成:
age
PropertyNamer.methodToProperty() 的逻辑是:如果方法名以 is 开头,去掉 is 前缀;如果以 get 或 set 开头,去掉 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() 的核心逻辑是:如果 condition 为 true,则执行传入的 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()
);
推荐断点:
AbstractWrapper.eq()AbstractWrapper.addCondition()AbstractWrapper.formatParam()MergeSegments.add()AbstractLambdaWrapper.getColumnCache()LambdaUtils.extract()PropertyNamer.methodToProperty()
浙公网安备 33010602011771号