• Mybatis - 预编译的运用和原理


    一. 什么是预编译

    首先我们来说下预编译的一个背景:我们知道一条SQL语句到达Mysql之后,Mysql并不是会马上执行它,而是需要经过几个阶段性的动作(细节的可以查看Mysql复习计划(一)- 字符集、文件系统和SQL执行流程):

    1. 缓存的检查。
    2. 解析器解析。
    3. 优化器解析。
    4. 执行器执行。

    那么这几个阶段肯定是需要一定的时间的。而有时候我们一条SQL语句可能需要反复的执行,只不过里面的参数可能不一样,比如where子句中的条件。如果每次都需要经过上面的几个步骤,那么效率就会下降。因此为了解决这种问题,就出现了预编译。

    预编译语句就是将这类语句中的值用占位符替代,可以视为将SQL语句模板化或者说参数化。一次编译、多次运行,省去了解析优化等过程。

    1.1 Mybatis中预编译的运用

    Mybatis中,默认是开启预编译的功能的,我们来测试一下(用的SpringBoot):

    1.pom依赖:

    <dependency>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>
    

    2.配置文件:application.yml文件。

    server:
      port: 8080
    
    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://IP地址:3306/数据库名称
        username: xxx
        password: xxx
    # 别名,这样写Mapper配置文件的时候可以省略前缀
    mybatis:
      type-aliases-package: com.application.bean
    

    application.properties文件:

    #druid数据库连接池
    type=com.alibaba.druid.pool.DruidDataSource
    #配置mapper
    mybatis.mapper-locations=classpath:mapper/*.xml
    #配置日志输出
    mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    

    3.Mapper.xml文件:
    对应目录:
    在这里插入图片描述
    文件内容:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!--关联UserMapper接口 ,下面的id要和接口中对应的方法名称一致,实现一对一的对应-->
    <mapper namespace="com.application.mapper.UserMapper">
        <select id="getUserById" parameterType="String" resultType="user">
            select * from user where userName=#{userName}
        </select>
    </mapper>
    
    

    4.实体类:
    在这里插入图片描述

    public class User {
        private String userName;
        private int id;
        // get/set
    }
    

    5.mapper接口:

    @Mapper
    @Repository
    public interface UserMapper {
        User getUserById(String userName);
    }
    

    6.Service类:

    @Service
    public class UserProcess {
        @Autowired
        private UserMapper userMapper;
    
        public User getUser(String name){
            return userMapper.getUserById(name);
        }
    }
    

    7.Controller类:

    @RestController
    public class MyController {
        @Autowired
        private UserProcess userProcess;
    
        @PostMapping("/hello")
        public User hello(){
            return userProcess.getUser("tom");
        }
    }
    

    8.运行后的日志输出:
    在这里插入图片描述
    当你看到了占位符 的时候,就说明预编译成功了,Mybatis就是将#{}这样的参数用占位符问号来代替。形成一个SQL模板。以达到预编译的目的。

    1.2 预编译的原理

    Mybatis中,对于SQL的处理解析动作在于XMLStatementBuilder.parseStatementNode这个类的函数中,我们来从这里为入口来看。我们先来看本文案例中的Mapper文件:

    <mapper namespace="com.application.mapper.UserMapper">
        <select id="getUserById" parameterType="String" resultType="user">
            select * from user where userName=#{userName}
        </select>
    </mapper>
    

    1.2.1 动态SQL的分类

    再看下源码:

    public void parseStatementNode() {
      // 命名空间中的唯一标识,一般可以用方法名
      String id = context.getStringAttribute("id");
      // 对应的数据库标识
      String databaseId = context.getStringAttribute("databaseId");
    
      if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
      }
      // 驱动程序每次批量返回的结果行数
      Integer fetchSize = context.getIntAttribute("fetchSize");
      // 等待数据库返回结果的超时时间
      Integer timeout = context.getIntAttribute("timeout");
      String parameterMap = context.getStringAttribute("parameterMap");
      // 该SQL中,传入的参数的完全限定名或者别名。本文是user
      String parameterType = context.getStringAttribute("parameterType");
      Class<?> parameterTypeClass = resolveClass(parameterType);
      // Map结果集
      String resultMap = context.getStringAttribute("resultMap");
      // 该SQL返回类型
      String resultType = context.getStringAttribute("resultType");
      String lang = context.getStringAttribute("lang");
      LanguageDriver langDriver = getLanguageDriver(lang);
    
      Class<?> resultTypeClass = resolveClass(resultType);
      // 结果集的相关配置,下文给出详细的点
      String resultSetType = context.getStringAttribute("resultSetType");
      // 语句类型的处理,下文给出详细的点
      StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
      ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    
      String nodeName = context.getNode().getNodeName();
      // 得到SQL命令的类型:对应着增删改查,有这么几种枚举值:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
      SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      // 设置为true的时候,只要语句被调用,就会让本地缓存和二级缓存被清空。默认值为false(针对select)
      boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
      // true代表将本次查询的语句通过二级缓存缓存起来,针对select,为true。
      boolean useCache = context.getBooleanAttribute("useCache", isSelect);
      boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
      // Include Fragments before parsing
      XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
      includeParser.applyIncludes(context.getNode());
    
      // Parse selectKey after includes and remove them.
      processSelectKeyNodes(id, parameterTypeClass, langDriver);
      
      // 生成SQL的地方。
      SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
      // ...省略
    }
    

    resultSetType有哪些类型?

    • FORWARD_ONLY:结果集的游标只能向下滚动。
    • SCROLL_INSENSITIVE:结果集的游标可以上下移动,当数据库变化时,当前结果集不变。
    • SCROLL_SENSITIVE:返回可滚动的结果集,当数据库变化时,当前结果集同步改变。

    statementType有哪些类型?

    • STATEMENT:普通语句。
    • PREPARED:预处理。
    • CALLABLE:存储过程。

    那么本文只关注于预编译的处理过程。我们继续看上述函数中的关键代码:

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    

    这段代码就是通过LanguageDriverSQL语句进行解析,返回的结果中,就可能包含动态SQL或者占位符。我们来看下这个函数的源码:LanguageDriver的默认实现是XMLLanguageDriver

    public class XMLLanguageDriver implements LanguageDriver {
      // 接着到这里
      @Override
      public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        // 创建一个XMLScriptBuilder对象。并通过它来解析SQL脚本
        XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
        return builder.parseScriptNode();
      }
      ↓↓↓↓↓↓↓
      public SqlSource parseScriptNode() {
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource = null;
        // 这里就是重点了,判断这个SQL是否是动态的
        if (isDynamic) {
          sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
          sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
      }
    }
    

    从上面源码可以得出,最终SQL构建起来,有两个分支:

    • 动态的:通过DynamicSqlSource来构建。
    • 非动态的:通过RawSqlSource来构建。

    那么是否为动态的判断标准是啥呢?关键看下面这行代码:

    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    ↓↓↓↓↓↓↓
    protected MixedSqlNode parseDynamicTags(XNode node) {
      List<SqlNode> contents = new ArrayList<SqlNode>();
      NodeList children = node.getNode().getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
          String data = child.getStringBody("");
          // 将文本节点封装成TextSqlNode ,然后判断是否是动态的
          TextSqlNode textSqlNode = new TextSqlNode(data);
          if (textSqlNode.isDynamic()) {
            contents.add(textSqlNode);
            isDynamic = true;
          } else {
            contents.add(new StaticTextSqlNode(data));
          }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
          String nodeName = child.getNode().getNodeName();
          NodeHandler handler = nodeHandlerMap.get(nodeName);
          if (handler == null) {
            throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
          }
          handler.handleNode(child, contents);
          isDynamic = true;
        }
      }
      return new MixedSqlNode(contents);
    }
    ↓↓↓↓↓↓↓
    textSqlNode.isDynamic()
    ↓↓↓↓↓↓↓
    public class TextSqlNode implements SqlNode {
      public boolean isDynamic() {
        DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
        GenericTokenParser parser = createParser(checker);
        parser.parse(text);
        return checker.isDynamic();
      }
      ↓↓↓↓↓↓↓
      private GenericTokenParser createParser(TokenHandler handler) {
        return new GenericTokenParser("${", "}", handler);
      }
    }
    

    到这里为止,我们可以观察到,在判断这个SQL是否为动态的时候,底层会创建一个GenericTokenParser类型的解析器。而这个解析器呢则是一个以 ${ 为开始和以 } 为结尾的解析器。如果解析成功,说明该SQL中包含了${},那么该SQL就会被标记为动态SQL标签。

    那么反之,我们回到这段代码中:
    在这里插入图片描述
    对于#{}这样的语句,就是一个非动态标签。总结下就是:

    • 如果SQL中的参数是用${}作为占位符的,那么该SQL属于动态SQL,封装为DynamicSqlSource
    • 否则其他的都是非动态SQL,封装为RawSqlSource

    1.2.2 预编译的处理(占位符的替换)

    那么我们来看下RawSqlSource的构造函数:

    public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
      SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
      Class<?> clazz = parameterType == null ? Object.class : parameterType;
      // SQL的解析
      sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
    }
    
    public class SqlSourceBuilder extends BaseBuilder {
      public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
        // 主要的一个解析器
        ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
        GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
        // 获取真实的可执行性的sql语句
        String sql = parser.parse(originalSql);
        // 包装成StaticSqlSource然后返回
        return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
      }
    }
    

    这里我们可以看出来,对于非动态SQL,会生成一个以 #{ 为开头,} 为结尾的解析器。紧接着就会创建一个StaticSqlSource 类,在这里做个区分:

    • RawSqlSource : 存储的是只有 #{} 或者没有标签的纯文本SQL信息
    • DynamicSqlSource : 存储的是写有 ${} 或者具有动态SQL标签的SQL信息
    • StaticSqlSource : 是DynamicSqlSourceRawSqlSource解析为BoundSql的一个中间态对象类型。
    • BoundSql:用于生成我们最终执行的SQL语句,属性包括参数值、映射关系、以及SQL(带问号的)

    接着我们看下解析器ParameterMappingTokenHandler 的处理,它是SqlSourceBuilder的一个静态内部类:

    private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
      @Override
      public String handleToken(String content) {
        parameterMappings.add(buildParameterMapping(content));
        return "?";
      }
    }
    

    这段代码在哪里用到呢?就是在GenericTokenParser.parse()这段代码中执行到的:

    public class GenericTokenParser {
      public String parse(String text) {
        // ...
        while (start > -1) {
          if (start > 0 && src[start - 1] == '\\') {
            // this open token is escaped. remove the backslash and continue.
            builder.append(src, offset, start - offset - 1).append(openToken);
            offset = start + openToken.length();
          } else {
            // ...
            if (end == -1) {
              // close token was not found.
              builder.append(src, start, src.length - start);
              offset = src.length;
            } else {
              // 就是找到了#{}的结束标识},然后将中间的内容替换成?
              builder.append(handler.handleToken(expression.toString()));
              offset = end + closeToken.length();
            }
          }
          start = text.indexOf(openToken, offset);
        }
        if (offset < src.length) {
          builder.append(src, offset, src.length - offset);
        }
        return builder.toString();
      }
    }
    

    这段代码执行完毕之后,我们发现最后返回了一个StaticSqlSource 对象。

    1.2.3 执行的时候如何替换参数(参数赋值)

    那么在执行SQL的时候,则会去根据BoundSql来完成参数的赋值等操作。我们来看下RawSqlSource.getBoundSql这个函数:

    public class RawSqlSource implements SqlSource {
      private final SqlSource sqlSource;
    
      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        return sqlSource.getBoundSql(parameterObject);
      }
    }
    

    因为只有#{}SQL语句,在上文中可以看到最后会生成一个StaticSqlSource对象,而这个类中就重写了getBoundSql函数,里面主要构造了一个BoundSql对象。

    public class StaticSqlSource implements SqlSource {
      @Override
      public BoundSql getBoundSql(Object parameterObject) {
        return new BoundSql(configuration, sql, parameterMappings, parameterObject);
      }
    }
    

    Mybatis在执行SQL的时候,主要看的是SimpleExecutor.prepareStatement这个入口:

    public class SimpleExecutor extends BaseExecutor {
      private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        // 获取JDBK的数据库连接
        Connection connection = getConnection(statementLog);
        // 准备工作,初始化Statement连接
        stmt = handler.prepare(connection, transaction.getTimeout());
        // 使用ParameterHandler处理入参
        handler.parameterize(stmt);
        return stmt;
      }
    }
    

    我们主要关注入参的处理函数上:

    public class PreparedStatementHandler extends BaseStatementHandler {
      @Override
      public void parameterize(Statement statement) throws SQLException {
        parameterHandler.setParameters((PreparedStatement) statement);
      }
    }
    ↓↓↓↓↓
    public class DefaultParameterHandler implements ParameterHandler {
      @Override
      public void setParameters(PreparedStatement ps) {
        ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
        // 从boundSql中拿到我们传入的参数
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings != null) {
          // 循环处理每一个参数,需要应用到类型转换器
          for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            // mode属性有三种:IN, OUT, INOUT。如果参数为 OUT 或 INOUT,参数对象属性的真实值将会被改变
            if (parameterMapping.getMode() != ParameterMode.OUT) {
              Object value;
              String propertyName = parameterMapping.getProperty();
              if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                value = boundSql.getAdditionalParameter(propertyName);
              } else if (parameterObject == null) {
                value = null;
              } 
              // 如果类型处理器里面有这个类型,直接赋值即可。
              else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
              } 
    		  // 否则转化为元数据处理,通过反射来完成get/set赋值
    		  else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
              }
              TypeHandler typeHandler = parameterMapping.getTypeHandler();
              JdbcType jdbcType = parameterMapping.getJdbcType();
              if (value == null && jdbcType == null) {
                jdbcType = configuration.getJdbcTypeForNull();
              }
              try {
                // 使用不同的类型处理器向jdbc中的PreparedStatement设置参数
                typeHandler.setParameter(ps, i + 1, value, jdbcType);
              } catch (TypeException e) {
                throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
              } catch (SQLException e) {
                throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
              }
            }
          }
        }
      }
    }
    

    前面一部分逻辑主要是对参数进行类型转换。最后则对SQL进行参数的赋值和替换。那么我们主要关注最后的赋值代码:

    typeHandler.setParameter(ps, i + 1, value, jdbcType);
    ↓↓↓↓↓
    public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
      @Override
      public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
        if (parameter == null) {
          if (jdbcType == null) {
            throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
          }
          try {
            ps.setNull(i, jdbcType.TYPE_CODE);
          } catch (SQLException e) {
            throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
                    "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. " +
                    "Cause: " + e, e);
          }
        } else {
          try {
            setNonNullParameter(ps, i, parameter, jdbcType);
          } catch (Exception e) {
            throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . " +
                    "Try setting a different JdbcType for this parameter or a different configuration property. " +
                    "Cause: " + e, e);
          }
        }
      }
    }
    

    我们重点关注那些非null值的赋值,看下Mybatis是怎么替换SQL中的 的:

    setNonNullParameter(ps, i, parameter, jdbcType);
    

    这个函数是一个抽象函数,有很多具体的实现,对应的参数是什么类型的,就用对应类型的处理方式去完成,如图:
    在这里插入图片描述
    无论是哪种类型,本质上都是对 占位符进行值的替换操作。

    1.3 总结

    因为本篇文章主要是将Mybatis对于预处理的原理。因此整个Mybatis的机制或者是二级缓存、一级缓存的知识点不在本文内容范围中(准备再写几篇内容补上)。本文主要讲了:什么是预编译。Mybatis中对预编译的处理、如何生成模板SQL、如何进行值的替换。

    因此这里做个小总结:

    首先预编译对于Mybatis而言,相当于构建出了一条SQL的模板,将#{}对应的参数改为?而已。届时只需要更改参数的值即可,无需再对SQL进行语法解析等操作。


    对于动态SQL的判断,就是在于是否包含${}占位符。如果包含了就通过DynamicSqlSource来解析。而这里则影响到SQL的解析:

    • DynamicSqlSource:解析包含${}的语句,其实也会解析#{}的语句。
    • RawSqlSource:解析只包含#{}的语句。

    这两种类型到最后都会转化为StaticSqlSource,然后由他创建一个BoundSql对象。包括参数值、映射关系、以及转化好的SQL


    最后是关于SQL的执行,即如何将真实的参数赋值到我们上面生成的模板SQL中。这部分逻辑发生在SQL的执行过程中,其入口SimpleExecutor.prepareStatement主要做了这么几件事。

    1. 根据我们上面生成的BoundSql对象。拿到我们传入的参数。
    2. 对每个参数进行解析,转化成对应的类型。
    3. 如果转化出的参数值为null,则直接赋值,否则,还要通过类型处理器来完成赋值操作。typeHandler.setParameter(ps, i + 1, value, jdbcType);
    4. 每种类型处理器,则会对对应的参数进行赋值。

    备注,Mybatis支持的类型处理器可以看TypeHandlerRegistry这个类,相关处理器的注册在其构造函数中。这里贴出部分截图:
    在这里插入图片描述

  • 相关阅读:
    kafka 磁盘扩容与数据均衡实在操作讲解
    python3-端口扫描(TCP connect扫描,SYN扫描,FIN扫描)
    Breakdance评测:改变游戏规则的WordPress网站生成器
    零基础小白怎么自学 Python ,大概要多久?
    QDateEdit开发详解
    药物从研发到上市需要经历哪些流程?||新药研发
    Selenium操作已经打开的Chrome浏览器窗口
    Windows Server使用WinSW注册服务自启动
    Python遇上SQL,于是一个好用的Python第三方库出现了
    数据结构-红黑树
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/127073676