MyBatis-Plus 源码阅读(三)条件构造器原理深度剖析

接上篇MyBatis-Plus 源码阅读(二)代码生成器原理深度剖析,本文我们聚焦 MP 中高频使用的条件构造器(Wrapper) ,从 SQL 配置、源码结构到执行流程,逐层拆解其工作原理。

环境

  • 核心依赖:mybatis-plus-spring-boot3-starter 3.5.14
  • 基础框架:Spring Boot 3.5.6
  • JDK 版本:17
  • 开发工具:IntelliJ IDEA

Wrapper 是什么?

条件构造器 | MyBatis-Plus 官方文档。

先回顾官方定义:Wrapper 是 MP 提供的条件构造工具类,支持链式调用构造 SQL 查询条件,无需手动拼接字符串,既提升开发效率,又能规避 SQL 注入风险。

举个直观例子:

java 复制代码
QueryWrapper<User> qw = new QueryWrapper<User>()
        .ge("id", 3)
        .eq("name", "happy")
        .last("limit 1");
        
userMapper.selectList(qw);

最终生成的 SQL:

sql 复制代码
SELECT 
id,name,password,date,delete_timestamp 
FROM user 
WHERE (id >= 3 AND name = 'happy') 
limit 1

相比传统 MyBatis 需要编写 XML / 注解 SQL,Wrapper 实现了动态条件的 "无感知" 构建------ 这背后的逻辑正是本文要拆解的重点。

源码分析:从 SQL 配置到条件构造

定位 selectList 的 SQL 模板

userMapper.selectList(qw) 最终调用的是 BaseMapper#selectList(Wrapper<T>),而该方法的 SQL 模板来自 MP 的内置注入逻辑。(参考MyBatis-Plus 源码阅读(一)CRUD 方法自动生成原理深度剖析

SelectList#injectMappedStatement 方法中,我们能看到核心 SQL 配置。

下面是 SQL 模板格式化后的内容。

xml 复制代码
<script>
   <if test="ew != null and ew.sqlFirst != null">${ew.sqlFirst}</if>
    SELECT 
   <choose>
       <when test="ew != null and ew.sqlSelect != null">${ew.sqlSelect}</when>
       <otherwise>
            id,
            name,
            password,
            date,
            delete_timestamp
       </otherwise>
   </choose>
    FROM user 
   <if test="ew != null">
       <bind name="_sgEs_" value="ew.sqlSegment != null and ew.sqlSegment != ''" />
       <where>
           <if test="ew.entity != null">
               <if test="ew.entity.id != null">id=#{ew.entity.id}</if>
               <if test="ew.entity['name'] != null">AND name=#{ew.entity.name}</if>
               <if test="ew.entity['password'] != null">AND password=#{ew.entity.password}</if>
               <if test="ew.entity['date'] != null">AND date=#{ew.entity.date}</if>
               <if test="ew.entity['deleteTimestamp'] != null">AND delete_timestamp=#{ew.entity.deleteTimestamp}</if>
           </if>
           <if test="_sgEs_ and ew.nonEmptyOfNormal">AND ${ew.sqlSegment}</if>
       </where>
       <if test="_sgEs_ and ew.emptyOfNormal">${ew.sqlSegment}</if>
   </if>
   <if test="ew != null and ew.sqlComment != null">${ew.sqlComment}</if>
</script>

关键观察:

  • ew 是 Wrapper 对象在 SQL 模板中的别名(通过 @Param("ew") 注入);
  • 第 25 行,${ew.sqlSegment} 是 Wrapper 构造的条件核心 ------我们写的 ge/eq 等方法,最终都会转化为 sqlSegment 的内容

补充说明:

  • _sgEs_:判断 sqlSegment 是否非空;
  • ew.nonEmptyOfNormal:标记是否包含普通查询条件(如 ge/eq);

Wrapper 代码分析

下面是 Wrapper 类简单的继承关系 AbstractWrapper 的泛型设计值得关注:

  • T:实体类类型(如 User);
  • R:数据库列名的表达类型(QueryWrapper 为 String,LambdaQueryWrapper 为 SFunction);
  • Children:子类自身类型(在本例中为 QueryWrapper<User>)。

本文以 QueryWrapper 为例分析(LambdaQueryWrapper 仅字段解析逻辑不同,核心原理一致)。

构造条件时发生了什么?

第一个条件 ge

.ge("id", 3) 为例,发现会进入com.baomidou.mybatisplus.core.conditions.AbstractWrapper#ge方法。最后将 3 个 ISqlSegment 的实现类添加到 AbstractWrapper 类的 expression 变量里。

experssion 的类型为 MergeSegments

java 复制代码
public class MergeSegments implements ISqlSegment {

    private final NormalSegmentList normal = new NormalSegmentList();
    private final GroupBySegmentList groupBy = new GroupBySegmentList();
    private final HavingSegmentList having = new HavingSegmentList();
    private final OrderBySegmentList orderBy = new OrderBySegmentList();
    // ...

geeq 这样的条件都是添加到 NormalSegmentList 里面的。

ISqlSegment 又是什么?

java 复制代码
@FunctionalInterface
public interface ISqlSegment extends Serializable {

    /**
     * SQL 片段
     */
    String getSqlSegment();

}

其实就是 mp 定义的一个函数式接口,用于返回 sql 片段。

在上面的代码中6,.ge("id", 3)这个代码添加的三个 ISqlSegment 的内容分别为 id, >=#{ew.paramNameValuePairs.MPGENVAL1}

注意,第三个 ISqlSegment 返回的内容不是 3 而是一个预编译的变量 ew.paramNameValuePairs.MPGENVAL1,并且在生成这个 ISqlSegment 的时候会调用 com.baomidou.mybatisplus.core.conditions.AbstractWrapper#formatParam 方法,将实际的参数值存入 paramNameValuePairs 变量。在上面例子中,参数名为 MPGENVAL1,值为 3

第二个条件 eq

当调用 .eq("name", "happy") 时,MP 会自动在两个条件间添加 AND 连接符.

因此,.ge().eq() 最终生成的片段序列为:id>=#{...MPGENVAL1}ANDname=#{...MPGENVAL2}

第三个条件 last

.last("limit 1") 更简单 ------ 直接将字符串赋值给 AbstractWrapperlastSql 属性,最终在 SQL 模板末尾拼接。

sqlSegment 的最终生成

当所有的条件构造完成之后,我们构造的 queryWrapper 通过 getSqlSegment() 方法获取到的内容应该就是

sql 复制代码
(id >= #{ew.paramNameValuePairs.MPGENVAL1} 
AND 
name = #{ew.paramNameValuePairs.MPGENVAL2}) 
limit 1

SQL 模板与条件的最终融合

Wrapper 构造的条件最终要与 SQL 模板结合,这一步由 MyBatis 的动态 SQL 引擎完成(核心在 DynamicSqlSource#getBoundSql

java 复制代码
public class DynamicSqlSource implements SqlSource {

  private final Configuration configuration;
  private final SqlNode rootSqlNode;

  public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
    this.configuration = configuration;
    this.rootSqlNode = rootSqlNode;
  }

  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    // 1. 将 ${} 的内容转换为实际内容,
    // 例如 ${ew.sqlSegment} 转换为 (id >= #{ew.paramNameValuePairs.MPGENVAL1} AND name = #{ew.paramNameValuePairs.MPGENVAL2}) limit 1
    rootSqlNode.apply(context); 
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    // 2. 处理 #{} 这样的预编译内容
    // 例如 (id >= #{ew.paramNameValuePairs.MPGENVAL1} AND name = #{ew.paramNameValuePairs.MPGENVAL2}) limit 1 转换为 (id >=? AND name = ?) limit 1
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

}

总结

通过分析,我们理清了 Wrapper 的核心逻辑:以链式调用收集条件片段 → 存储为预编译占位符形式 → 拼接为 sqlSegment → 与 MyBatis 动态 SQL 融合生成最终 SQL

除了本文分析的基础条件构造,Wrapper 还支持:

  • select("id", "name"):通过 sqlSelect 指定查询字段;
  • orderByAsc("id"):通过 orderBy 容器构造排序条件;
  • lambda():LambdaQueryWrapper 通过解析 User::getId 获取字段名,避免硬编码。

这些功能的底层逻辑与本文分析的条件构造一致 ------ 都是通过 ISqlSegment 片段容器和动态拼接实现。

结语

Wrapper 本质是 MP 为开发者封装的 "动态 SQL 构建 DSL",它将复杂的条件拼接逻辑隐藏在链式调用背后,既简化了代码,又保证了安全性。理解其原理后,无论是自定义条件构造器,还是排查复杂条件的拼接问题,都会更加得心应手。

如果这篇文章对你理解「条件构造器原理」有帮助,麻烦点赞 + 关注支持一下~ 你的认可就是我持续输出源码解析系列的最大动力!❤️❤️❤️🤩🤩🤩🚀🚀🚀

源码地址

相关推荐
zhaomy20252 小时前
从ThreadLocal到ScopedValue:Java上下文管理的架构演进与实战指南
java·后端
用户84913717547162 小时前
从源码看设计:Java 集合框架的安全性与性能权衡 (基于 JDK 1.8)
java·面试
华仔啊2 小时前
10分钟搞定!SpringBoot+Vue3 整合 SSE 实现实时消息推送
java·vue.js·后端
l***77522 小时前
总结:Spring Boot 之spring.factories
java·spring boot·spring
天若有情6732 小时前
笑喷!乌鸦哥版demo函数掀桌怒怼主函数:难办?那就别办了!
java·前端·servlet
SimonKing3 小时前
你的IDEA还缺什么?我离不开的这两款效率插件推荐
java·后端·程序员
better_liang3 小时前
每日Java面试场景题知识点之-数据库连接池配置优化
java·性能优化·面试题·hikaricp·数据库连接池·企业级开发
Wpa.wk3 小时前
自动化测试环境配置-java+python
java·开发语言·python·测试工具·自动化