在 MyBatis-plus 自定义通用方法及其实现原理 中笔者介绍了 MyBatis-plus 添加通用方法的实现方式,但是其中还有一些细节需要澄清,下文笔者将详细分析
1. MyBatis-plus 对 SQL 语句脚本的处理
- MyBatis-plus 对 SQL 语句脚本的构建,以及将其嵌入MappedStatement 的过程
- MyBatis 使用 SqlSource 构建可执行的 SQL 语句
我们都知道 MyBatis-plus 干掉了繁琐的 XML 文件,使 MyBatis 框架的易用度、好用度大幅上升。在MyBatis-plus 源码解析 中笔者提到过,MyBatis-plus 实际是将 Mapper 方法映射为了对应的 SQL 语句脚本,这个步骤的核心就是 AbstractMethod#injectMappedStatement() 的子类实现,本文以 SelectOne#injectMappedStatement() 为例进行分析,其主要处理分为两个部分:
1.1 SQL 语句脚本的构建
- SQL 语句脚本的构建
- 解析 SQL 语句脚本,将其转化为 SqlSource 封装到 MappedStatement 中
-
SelectOne#injectMappedStatement() 方法如下,SQL 语句脚本的构建实际在一个以 SqlMethod.SELECT_ONE 为主体的 String#format() 拼接操作中,重要调用的如下:
- 通过 sqlMethod.getSql() 调用 SqlMethod.SELECT_ONE#getSql() 方法获得 SQL 脚本主体字符串
- 调用 AbstractMethod#sqlFirst() 构建 SQL 脚本在正式 SQL 语句之前的部分,这里会使用字符串替换 ${ew.sqlFirst}
- 调用 AbstractMethod#sqlSelectColumns() 构建 SQL 语句 SELECT 查询的字段相关的脚本部分,此处会使用字符串替换 ${ew.sqlSelect}
- 调用 TableInfo#getTableName() 获取实际的要查询的表名
- 调用 AbstractMethod#sqlWhereEntityWrapper() 构建 SQL 语句 WHERe 条件相关的脚本部分,此处会使用字符串替换 ${ew.sqlSegment}
- 调用 AbstractMethod#sqlComment() 构建 SQL 语句尾部注释相关的脚本部分,此处会使用字符串替换 ${ew.sqlComment}
@Override public MappedStatement injectMappedStatement(Class> mapperClass, Class> modelClass, TableInfo tableInfo) { SqlMethod sqlMethod = SqlMethod.SELECT_ONE; SqlSource sqlSource = languageDriver.createSqlSource(configuration, String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns(tableInfo, true), tableInfo.getTableName(), sqlWhereEntityWrapper(true, tableInfo), sqlComment()), modelClass); return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo); } -
SqlMethod.SELECT_ONE#getSql() 方法实际只是个获取操作,只要看下这个枚举的定义就知道这里拿到的是个使用 %s 占位的脚本字符串
public enum SqlMethod { ...... SELECT_ONE("selectOne", "查询满足条件一条数据", ""), private final String method; private final String desc; private final String sql; SqlMethod(String method, String desc, String sql) { this.method = method; this.desc = desc; this.sql = sql; } public String getMethod() { return method; } public String getDesc() { return desc; } public String getSql() { return sql; } } -
AbstractMethod#sqlFirst() 的实现很简单,可以看到这里实际就是使用 SqlscriptUtils#convertChoose() 工具类拼接 SQL 语句脚本 标签的过程,经过处理这里可以得到的脚本片段如下
${ew.sqlFirst} protected String sqlFirst() { return SqlscriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_FIRST), SqlscriptUtils.unSafeParam(Q_WRAPPER_SQL_FIRST), EMPTY); } -
SqlscriptUtils#convertChoose() 方法如下,显然就是标签字符串的构造,没有特别操作
public static String convertChoose(final String whenTest, final String whenSqlscript, final String otherwise) { return "" + newline + " "; }" + newline + " " + otherwise + " " + newline + " -
AbstractMethod#sqlSelectColumns() 的处理大同小异,其实就是指定查询表的字段,此处可以得到如下脚本片段
${ew.sqlSelect} id,name,type protected String sqlSelectColumns(TableInfo table, boolean queryWrapper) { String selectColumns = ASTERISK; if (table.getResultMap() == null || (table.getResultMap() != null && table.isInitResultMap())) { selectColumns = table.getAllSqlSelect(); } if (!queryWrapper) { return selectColumns; } return SqlscriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_SELECT), SqlscriptUtils.unSafeParam(Q_WRAPPER_SQL_SELECT), selectColumns); } -
AbstractMethod#sqlWhereEntityWrapper() 的处理稍显复杂,不过原理和以上方法是一样的,此处可以获得如下脚本片段
id=#{ew.entity.id} AND name=#{ew.entity.name} AND type=#{ew.entity.type} AND ${ew.sqlSegment}${ew.sqlSegment} protected String sqlWhereEntityWrapper(boolean newline, TableInfo table) { if (table.isLogicDelete()) { String sqlscript = table.getAllSqlWhere(true, true, WRAPPER_ENTITY_DOT); sqlscript = SqlscriptUtils.convertIf(sqlscript, String.format("%s != null", WRAPPER_ENTITY), true); sqlscript += (newline + table.getLogicDeleteSql(true, true) + newline); String normalSqlscript = SqlscriptUtils.convertIf(String.format("AND ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_NONEMPTYOFNORMAL), true); normalSqlscript += newline; normalSqlscript += SqlscriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_EMPTYOFNORMAL), true); sqlscript += normalSqlscript; sqlscript = SqlscriptUtils.convertChoose(String.format("%s != null", WRAPPER), sqlscript, table.getLogicDeleteSql(false, true)); sqlscript = SqlscriptUtils.convertWhere(sqlscript); return newline ? newline + sqlscript : sqlscript; } else { String sqlscript = table.getAllSqlWhere(false, true, WRAPPER_ENTITY_DOT); sqlscript = SqlscriptUtils.convertIf(sqlscript, String.format("%s != null", WRAPPER_ENTITY), true); sqlscript += newline; sqlscript += SqlscriptUtils.convertIf(String.format(SqlscriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_NONEMPTYOFWHERe), true); sqlscript = SqlscriptUtils.convertWhere(sqlscript) + newline; sqlscript += SqlscriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT), String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT, WRAPPER_EMPTYOFWHERe), true); sqlscript = SqlscriptUtils.convertIf(sqlscript, String.format("%s != null", WRAPPER), true); return newline ? newline + sqlscript : sqlscript; } } -
AbstractMethod#sqlComment() 拿到的脚本片段如下,不做更多解释
${ew.sqlComment} protected String sqlComment() { return SqlscriptUtils.convertChoose(String.format("%s != null and %s != null", WRAPPER, Q_WRAPPER_SQL_COMMENT), SqlscriptUtils.unSafeParam(Q_WRAPPER_SQL_COMMENT), EMPTY); }
经过以上步骤,SQL 语句脚本各个关键的片段都已经构建完毕,最终得到的脚本如下所示。接下来的处理就是解析这个脚本,通过 LanguageDriver#createSqlSource() 方法将脚本转化为 SqlSource 对象,这个对象最终决定执行的 SQL 语句的重中之重
1.2 SqlSource 的转化
-
LanguageDriver#createSqlSource() 是接口方法,MyBatis 使用 XML 来定义 SQL 语句配置的,实际调用到 XMLLanguageDriver#createSqlSource(),可以看到这里最终调用 XMLscriptBuilder#parsescriptNode() 开始解析 XML 脚本
public SqlSource createSqlSource(Configuration configuration, String script, Class> parameterType) { // issue #3 if (script.startsWith("


