MyBatis-plus 转化处理 SQL 语句的源码分析

文章目录

  • 前言
  • 1. MyBatis-plus 对 SQL 语句脚本的处理
      • 1.1 SQL 语句脚本的构建
      • 1.2 SqlSource 的转化
  • 2. MyBatis 对 SqlSource 的使用

前言

在 MyBatis-plus 自定义通用方法及其实现原理 中笔者介绍了 MyBatis-plus 添加通用方法的实现方式,但是其中还有一些细节需要澄清,下文笔者将详细分析

  1. MyBatis-plus 对 SQL 语句脚本的构建,以及将其嵌入MappedStatement 的过程
  2. MyBatis 使用 SqlSource 构建可执行的 SQL 语句

MyBatis-plus 转化处理 SQL 语句的源码分析_第1张图片

1. MyBatis-plus 对 SQL 语句脚本的处理

我们都知道 MyBatis-plus 干掉了繁琐的 XML 文件,使 MyBatis 框架的易用度、好用度大幅上升。在MyBatis-plus 源码解析 中笔者提到过,MyBatis-plus 实际是将 Mapper 方法映射为了对应的 SQL 语句脚本,这个步骤的核心就是 AbstractMethod#injectMappedStatement() 的子类实现,本文以 SelectOne#injectMappedStatement() 为例进行分析,其主要处理分为两个部分:

  1. SQL 语句脚本的构建
  2. 解析 SQL 语句脚本,将其转化为 SqlSource 封装到 MappedStatement

1.1 SQL 语句脚本的构建

  1. SelectOne#injectMappedStatement() 方法如下,SQL 语句脚本的构建实际在一个以 SqlMethod.SELECT_ONE 为主体的 String#format() 拼接操作中,重要调用的如下:

    1. 通过 sqlMethod.getSql() 调用 SqlMethod.SELECT_ONE#getSql() 方法获得 SQL 脚本主体字符串
    2. 调用 AbstractMethod#sqlFirst() 构建 SQL 脚本在正式 SQL 语句之前的部分,这里会使用字符串替换 ${ew.sqlFirst}
    3. 调用 AbstractMethod#sqlSelectColumns() 构建 SQL 语句 SELECT 查询的字段相关的脚本部分,此处会使用字符串替换 ${ew.sqlSelect}
    4. 调用 TableInfo#getTableName() 获取实际的要查询的表名
    5. 调用 AbstractMethod#sqlWhereEntityWrapper() 构建 SQL 语句 WHERE 条件相关的脚本部分,此处会使用字符串替换 ${ew.sqlSegment}
    6. 调用 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);
     }
    
  2. 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;
     }
    }
    
  3. 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);
     }
    
  4. 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
             + "";
     }
    
  5. 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);
     }
    
  6. 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;
         }
     }
    
  7. 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>

1.2 SqlSource 的转化

  1. LanguageDriver#createSqlSource() 是接口方法,MyBatis 使用 XML 来定义 SQL 语句配置的,实际调用到 XMLLanguageDriver#createSqlSource(),可以看到这里最终调用 XMLScriptBuilder#parseScriptNode() 开始解析 XML 脚本

     public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
     // issue #3
     if (script.startsWith("
                        
                        

你可能感兴趣的:(MyBatis,sql,数据库,java,1024程序员节)