01o00o10

MyBatis-Plus 源码阅读(五):一条通用 CRUD SQL 是怎么拼出来的

MyBatis-Plus 源码阅读(五):一条通用 CRUD SQL 是怎么拼出来的

上一篇停在 AbstractMethod.inject()。这一篇往前再走一步,看 SQL 本身是怎么来的。

先拿 selectList 开刀,因为它把表名、查询列、Wrapper、逻辑删除和排序都串起来了。

SelectList 其实很短

SelectList.injectMappedStatement() 的核心只有两步:

String sql = getSql(tableInfo);
SqlSource sqlSource =
    createSqlSource(configuration, sql, modelClass);

getSql() 使用 SqlMethod.SELECT_LIST 中的模板,再通过 sqlSelectColumns(tableInfo, true) 生成查询列片段。第二个参数 true 表示查询时包含逻辑删除字段(逻辑删除的过滤条件由 WHERE 子句处理,但查询列本身仍然可以包含该字段)。

把动态标签先省略,它的骨架就是:

SELECT 查询列
FROM 表名
WHERE 条件
ORDER BY 排序

SqlMethod 是模板仓库

SqlMethod 是一个枚举,集中保存通用方法名、说明和 SQL 模板。3.5.6 版本的完整枚举列表如下:

INSERT_ONE
DELETE_BY_ID
DELETE_BY_MAP
DELETE
DELETE_BATCH_BY_IDS
LOGIC_DELETE_BY_ID
LOGIC_DELETE_BY_MAP
LOGIC_DELETE
LOGIC_DELETE_BATCH_BY_IDS
UPDATE_BY_ID
UPDATE
SELECT_BY_ID
SELECT_BY_MAP
SELECT_BATCH_BY_IDS
SELECT_ONE
SELECT_COUNT
SELECT_LIST
SELECT_MAPS
SELECT_MAPS_PAGE
SELECT_OBJS
SELECT_PAGE

每个枚举值的模板都包含 %s 占位符,例如 SELECT_LIST 的模板是:

<script>SELECT %s FROM %s %s</script>

%s 会在运行时被 String.format() 替换为具体的查询列、表名和 WHERE 片段。

它不是最终 SQL。模板中还有表名、字段片段和 MyBatis 动态标签,需要 AbstractMethod 的工具方法填进去。

TableInfo 在这里派上用场

SelectList 生成 SQL 时会问 TableInfo:表名是什么、默认查询哪些列、有没有逻辑删除字段、有没有默认排序。

Insert 会问主键是否参与插入、哪些字段允许插入、字段为空时要不要跳过、是否需要数据库 KeyGenerator。

UpdateById 会问主键列叫什么、哪些字段允许更新、有没有版本字段和逻辑删除条件。

所以 SQL Method 类不需要再反射实体,它消费的是前面已经整理好的元数据。

为什么要生成动态 SQL

实体字段是否为空、Wrapper 有没有条件,这些信息启动时还不知道。

因此 MyBatis-Plus 生成的是带动态标签的脚本:

<script>
UPDATE mp_user
<set>
  <if test="et.name != null">
    name = #{et.name},
  </if>
</set>
WHERE id = #{et.id}
</script>

LanguageDriver.createSqlSource() 会把脚本解析成 SqlSource。真正执行时,再根据参数得到 BoundSql

这里要分清三个东西:

SQL 模板:MyBatis-Plus 生成的动态脚本
SqlSource:MyBatis 解析脚本后的对象
BoundSql:某次调用带入参数后得到的 SQL 和参数映射

启动时有 SqlSource,调用时才有本次请求对应的 BoundSql

字段策略在哪生效

假设 name 字段使用“非空才更新”的策略:

@TableField(updateStrategy = FieldStrategy.NOT_NULL)
private String name;

MyBatis-Plus 会在 UPDATE 脚本中生成对应的 <if>。参数为空时,MyBatis 动态 SQL 不输出这段 SET。

这里需要注意一个版本变化:在 MyBatis-Plus 3.1.2 之前,字段策略统一由 fieldStrategy 控制;3.1.2 之后拆分为 insertStrategyupdateStrategyselectStrategy 三个独立策略。updateStrategy 默认值是 NOT_NULL,即只有字段值不为 null 时才参与 UPDATE SET 子句。如果想让某个字段始终参与更新(包括 null 值),可以设置为 FieldStrategy.IGNOREDFieldStrategy.ALWAYS

所以调试 UpdateById 时,只看 SQL 模板还不够。还要带着实际实体调用 getBoundSql(parameter),才能看到最终 SQL。

不同删除方式在这里分叉

DeleteById 会检查 tableInfo.isWithLogicDelete()

没有逻辑删除时:

DELETE FROM mp_user WHERE id = ?

有逻辑删除时:

UPDATE mp_user
SET deleted = 1
WHERE id = ? AND deleted = 0

实际生成的 SQL 中,deleted 字段名和删除值由 @TableLogic 注解或全局配置决定。例如配置 logic-delete-field: deletedlogic-delete-value: 1logic-not-delete-value: 0 后,调用 deleteById(1) 生成的 SQL 是 UPDATE user2 SET deleted=1 WHERE id=? AND deleted=0

所以方法名叫 deleteById,不代表 MyBatis 最终收到的一定是 DELETE。

Wrapper 是怎么插进模板的

AbstractMethod.sqlWhereEntityWrapper() 会生成一段动态 SQL,用约定名称 ew 访问 Wrapper:

ew.entity
ew.sqlSegment
ew.nonEmptyOfWhere
ew.paramNameValuePairs

调用 selectList(wrapper) 时,Mapper 参数解析器把 Wrapper 放到 ew 这个 key 下。这个约定来自 BaseMapper 接口方法上的 @Param(Constants.WRAPPER) 注解,Constants.WRAPPER 的值就是字符串 "ew"(Entity Wrapper 的缩写)。

动态脚本再从中读取 SQL 片段和参数。

动手看 BoundSql

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

Map<String, Object> params = new HashMap<>();
params.put(Constants.WRAPPER, wrapper);

String id =
    UserMapper.class.getName() + ".selectList";

MappedStatement ms =
    sqlSessionFactory.getConfiguration()
        .getMappedStatement(id);

BoundSql boundSql = ms.getBoundSql(params);

System.out.println(boundSql.getSql());
System.out.println(
    wrapper.getParamNameValuePairs()
);

推荐断点:

  1. SelectList.injectMappedStatement()
  2. AbstractMethod.sqlWhereEntityWrapper()
  3. AbstractMethod.createSqlSource()
  4. XMLLanguageDriver.createSqlSource(),这是 MyBatis 类
  5. MappedStatement.getBoundSql(),这是 MyBatis 类

前三个断点看 MyBatis-Plus 如何造脚本,后两个断点看 MyBatis 如何把脚本变成本次调用的 SQL。

posted on 2026-09-21 19:06  01o00o10  阅读(4)  评论(0)    收藏  举报

导航