在 MyBatis-plus 自定义通用方法及其实现原理 中笔者介绍了 MyBatis-plus
添加通用方法的实现方式,但是其中还有一些细节需要澄清,下文笔者将详细分析
MyBatis-plus
对 SQL 语句脚本的构建,以及将其嵌入MappedStatement
的过程MyBatis
使用SqlSource
构建可执行的 SQL 语句
我们都知道 MyBatis-plus
干掉了繁琐的 XML 文件,使 MyBatis
框架的易用度、好用度大幅上升。在MyBatis-plus 源码解析 中笔者提到过,MyBatis-plus
实际是将 Mapper 方法映射为了对应的 SQL 语句脚本,这个步骤的核心就是 AbstractMethod#injectMappedStatement()
的子类实现,本文以 SelectOne#injectMappedStatement()
为例进行分析,其主要处理分为两个部分:
- 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 语句脚本 标签的过程,经过处理这里可以得到的脚本片段如下
<choose>
<when test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
when>
<otherwise>otherwise>
choose>
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
+ " + whenTest + QUOTE + RIGHT_CHEV + NEWLINE
+ whenSqlScript + NEWLINE + " " + NEWLINE
+ "" + otherwise + "" + NEWLINE
+ "";
}
AbstractMethod#sqlSelectColumns()
的处理大同小异,其实就是指定查询表的字段,此处可以得到如下脚本片段
<choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
when>
<otherwise>id,name,typeotherwise>
choose>
protected String sqlSelectColumns(TableInfo table, boolean queryWrapper) {
/* 假设存在 resultMap 映射返回 */
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()
的处理稍显复杂,不过原理和以上方法是一样的,此处可以获得如下脚本片段
<if test="ew != null">
<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['type'] != null">AND type=#{ew.entity.type}if>
if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal">ANDif>
${ew.sqlSegment}
if>
where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
if>
if>
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()
拿到的脚本片段如下,不做更多解释
<choose>
<when test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
when>
<otherwise>otherwise>
choose>
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 语句的重中之重
<script>
<choose>
<when test="ew != null and ew.sqlFirst != null">
${ew.sqlFirst}
</when>
<otherwise></otherwise>
</choose>
SELECT
<choose>
<when test="ew != null and ew.sqlSelect != null">
${ew.sqlSelect}
</when>
<otherwise>id,name,type</otherwise>
</choose>
FROM node
<if test="ew != null">
<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['type'] != null">AND type=#{ew.entity.type}</if>
</if>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfWhere">
<if test="ew.nonEmptyOfEntity and ew.nonEmptyOfNormal">AND</if>
${ew.sqlSegment}
</if>
</where>
<if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfWhere">
${ew.sqlSegment}
</if>
</if>
<choose>
<when test="ew != null and ew.sqlComment != null">
${ew.sqlComment}
</when>
<otherwise></otherwise>
</choose>
script>
LanguageDriver#createSqlSource()
是接口方法,MyBatis
使用 XML 来定义 SQL 语句配置的,实际调用到 XMLLanguageDriver#createSqlSource()
,可以看到这里最终调用 XMLScriptBuilder#parseScriptNode()
开始解析 XML 脚本
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("