栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

前言

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

  1. MyBatis-plus 对 SQL 语句脚本的构建,以及将其嵌入MappedStatement 的过程
  2. MyBatis 使用 SqlSource 构建可执行的 SQL 语句
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 语句脚本 标签的过程,经过处理这里可以得到的脚本片段如下

    
         
             ${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);
     }
    
  4. SqlscriptUtils#convertChoose() 方法如下,显然就是标签字符串的构造,没有特别操作

      public static String convertChoose(final String whenTest, final String whenSqlscript, final String otherwise) {
         return "" + newline
             + "" + newline
             + "" + otherwise + "" + newline
             + "";
     }
    
  5. 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);
     }
    
  6. 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;
         }
     }
    
  7. 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 的转化
  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("