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 之后拆分为 insertStrategy、updateStrategy、selectStrategy 三个独立策略。updateStrategy 默认值是 NOT_NULL,即只有字段值不为 null 时才参与 UPDATE SET 子句。如果想让某个字段始终参与更新(包括 null 值),可以设置为 FieldStrategy.IGNORED 或 FieldStrategy.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: deleted、logic-delete-value: 1、logic-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()
);
推荐断点:
SelectList.injectMappedStatement()AbstractMethod.sqlWhereEntityWrapper()AbstractMethod.createSqlSource()XMLLanguageDriver.createSqlSource(),这是 MyBatis 类MappedStatement.getBoundSql(),这是 MyBatis 类
前三个断点看 MyBatis-Plus 如何造脚本,后两个断点看 MyBatis 如何把脚本变成本次调用的 SQL。
浙公网安备 33010602011771号