zoukankan      html  css  js  c++  java
  • mybatis 动态SQL 源码解析

    摘要

         mybatis是个人最新喜欢的半自动ORM框架,它实现了SQL和业务逻辑的完美分割,今天我们来讨论一个问题,mybatis 是如何动态生成SQL

    SqlSessionManager sqlSessionManager;
    DataSource dataSource = new PooledDataSource(driver,url,username,pass);
    TransactionFactory transactionFactory = new JdbcTransactionFactory();
    Environment environment = new Environment("development", transactionFactory, dataSource);
    Configuration configuration = new Configuration(environment);

    //设定解析mpper.xml位置
    String resource = "mybatis/ApplicationMapper.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
    mapperParser.parse();


    // 解析 ****mapper.xml中SQL入口(这里偷懒懒,直接组装好configuration啦)
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
    // 执行SQL的入口方法 步骤①:获取参数,②:拼接SQL,③:执行
    BizApp bizApp = sqlSessionFactory.openSession().getMapper(ApplicationMapper.class).queryByAppid("1000002715");

      mybatis 解析mpper.xml中内容,动态生成SQL由2部分组成

        ①:解析动态SQL 解析xml 保存各种信息到XMLMapperBuilder和Configuration属性中,主要xml 节点保存在以下属性中

              sql 解析  保存位置XmlMapperBuilderMap<String, XNode> sqlFragments
              resultMap 解析 保存位置 XmlMapperBuilder类属性Configuration Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
              select/update/insert/delete 解析 保存位置 XmlMapperBuilder类属性Configuration Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

        ②:执行前,拼接动态SQL

      听起来很简单,但是mybatis源码看起来还是相当复杂的,很佩服mybatis 的源码开发人员(膜拜)

    mybatis解析方法

      mybatis 中之所以存在动态SQL,最主要的原因是因为,mybatis给大家提供了动态标签,这个给大家使用Mybatis 带来了很大的便利,但是这个也大大提高了解析xml中SQL的难度

    • if
    • choose (when, otherwise)
    • trim (where, set)
    • foreach

      mybatis 主要采用OGNL+组合模式来完成解析的,我们直接上手源码看一下

          如:解析以下ApplicationMapper.xml中的SQL,mybatis流程什么样的 ???

     <sql id="Base_Column_List">
         id,biz_code,appid,appname,domain,sort,isactive, inserttime, updatetime
        </sql>
    
      //要解析的SQL SQL_TEST 开始 <select id="queryByAppid" resultMap="BaseResultMap"> select <include refid="Base_Column_List"/> from application where isactive=1 <if test="appid!=null"> and appid=#{appid} </if> </select>
    //SQL_TEST 结束

    //解析ApplicationMapper.xml入口方法

      SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(InputStream inputStream)
        --->parser.parse()
        --->parseConfiguration(parser.evalNode("/configuration"))
        --->mapperElement(root.evalNode("mappers"));
        --->mapperParser.parse();
        --->configurationElement(parser.evalNode("/mapper"));

        //最终解析ApplicationMapper.xml方法处
      private void configurationElement(XNode context) {
        try {
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
          }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));

           //动态标签解析处,最终调用方法parseStatementNode
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

      } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
      }
    }

    实际代码解析处

    public void parseStatementNode() {
    省略.....

    // 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);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    /**
    重点代码,
    ①:调用XMLLanguageDriver的createSqlSource
    ②:builder.parseScriptNode(); 解析具体SQL内容,具体请看下面分析

    **/
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    省略......
    }

      解析XML内容,并分解SQL

    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    builder.parseScriptNode();
    public SqlSource parseScriptNode() {
    //这句话就是解析上面SQL_TEST结果
    //后面拼接完整SQL就靠它啦,具体怎么玩,后面分析 MixedSqlNode rootSqlNode
    = parseDynamicTags(context);
        SqlSource sqlSource = null;
        if (isDynamic) {
    //只要XML中有动态标签就选择DynamicSqlSource
    //DynamicSqlSource和RawSqlSource 不同点在于只有在执行前才知道完整预编译的SQL sqlSource
    = new DynamicSqlSource(configuration, rootSqlNode); } else { sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); } return sqlSource; }
    
    

        以上是mybatis解析动态SQL完整流程,只将动态SQL分解啦,其他什么都没做

       下面是执行前拼接动态SQL的完整流程 

     //执行方法,sqlSessionFactory.openSession().getMapper(ApplicationMapper.class).queryByAppid("1000002715","appName"); 
    //执行上述方法,其实这是一个动态代理的过程,最终调用MapperProxy.invoke方法
    @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); }
    //参数名称解析
    //主要如何定义参数名称和设置值,方法queryByAppid("1000002715","appName"); 
    final MapperMethod mapperMethod = cachedMapperMethod(method);

    //方法执行
    return mapperMethod.execute(sqlSession, args); }
     //参数名称源码分析
    private MapperMethod cachedMapperMethod(Method method) { MapperMethod mapperMethod = methodCache.get(method); if (mapperMethod == null) { mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()); methodCache.put(method, mapperMethod); } return mapperMethod; } public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); } public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) { //省略....
    //重点代码
    this.paramNameResolver = new ParamNameResolver(configuration, method); } //最终解析方法名处 public ParamNameResolver(Configuration config, Method method) {
    //参数数组
    final Class<?>[] paramTypes = method.getParameterTypes();
    //每个参数前注解数组
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    //参数名存放地点
    final SortedMap<Integer, String> map = new TreeMap<Integer, String>(); int paramCount = paramAnnotations.length; // get names from @Param annotations for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { if (isSpecialParameter(paramTypes[paramIndex])) { // skip special parameters continue; } String name = null; for (Annotation annotation : paramAnnotations[paramIndex]) {
    //如果参数前有有Param注解,参数名为注解中value值
    if (annotation instanceof Param) { hasParamAnnotation = true; name = ((Param) annotation).value(); break; } } if (name == null) { // @Param was not specified.
    // 木有@Param注解,则参数名为arg0,arg1......
    if (config.isUseActualParamName()) { name = getActualParamName(method, paramIndex); } if (name == null) { // use the parameter index as the name ("0", "1", ...) // gcode issue #71 name = String.valueOf(map.size()); } } map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map);
    }

    最终效果类似如下:names 中存放的值,key:参数位置,value:参数名称

    //方法执源码分析
    //①将参数名和参数值对应起来,
    //②动态拼接SQL,
    //③SQL执行

    mapperMethod.execute(sqlSession, args);
    public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { 省略..... } case UPDATE: { 省略..... } case DELETE: { 省略..... } case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { //将参数名和参数值对应起来,效果如下,param 存放的值
             

             Object param = method.convertArgsToSqlCommandParam(args);

             //具体拼接SQL和执行SQL,底层调用的BaseExecutor.query方法

             result = sqlSession.selectOne(command.getName(), param);

            }
            break;
          case FLUSH:
            result = sqlSession.flushStatements();
            break;
          default:
            throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
          throw new BindingException("Mapper method '" + command.getName() 
              + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
      }
      //BaseExecutor.query方法 
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //具体拼接SQL BoundSql boundSql
    = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } public BoundSql getBoundSql(Object parameterObject) { BoundSql boundSql = sqlSource.getBoundSql(parameterObject); 省略..... return boundSql; } //这里调用的是DynamicSqlSource.getBoundSql,因为xmL中有动态标签
    //这里采用经典组合模式+OGNL完成标签解析和拼接完整SQL,终于大结局啦
    @Override
    public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject);
    //组合模式,并将SQL拼接结果存放到context中   rootSqlNode.apply(context); SqlSourceBuilder sqlSourceParser
    = new SqlSourceBuilder(configuration); Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) { boundSql.setAdditionalParameter(entry.getKey(), entry.getValue()); } return boundSql; }

    这里的SQL 拼接,解释一下,首先这个
    rootSqlNode 为MixedSqlNode 类型,(不知道说的是啥,看前面的分解SQL内容贴的debug调试图
    @Override
    public boolean apply(DynamicContext context) {
    for (SqlNode sqlNode : contents) {
    sqlNode.apply(context);
    }
    return true;
    }

    MixedSqlNode 这个类有一个集合属性List<SqlNode> contents,apply 就是遍历它,普通的文本为StaticTextSqlNode在组合模式中这个就是叶子节点,不需要任何转换直接拼接SQL

    我们XML有IF动态标签,所以它会调用IfSqlNode.apply 方法

    @Override
    public boolean apply(DynamicContext context) {
    //调用OGNL解析if标签,test文本,是否满足条件<if test="appid!=null">
    if (evaluator.evaluateBoolean(test, context.getBindings())) {
    //符合条件,向下遍历
    contents.apply(context);
    return true;
    }
    return false;
    }

     总结

             mybatis动态SQL生成,它主要是通过遍历XML中每行语句(某个具体的语句),遇到纯文本,直接封装成StaticTextSqlNode节点(普通文本,叶子节点,可直接拼接SQL)

      遇到动态标签,直接封装成IfSqlNode,TrimSqlNode......(树枝节点,需要靠OGNL来解析文本,视解析结果来拼接SQL),以及包含上述所有的节点的根节点

    (MixedSqlNode),拼接SQL时,只要从根节点向下遍历即可拼接出完整SQL

        //SQL_TEST 结束
    StaticTextSqlNode
  • 相关阅读:
    多数据源源路由方案
    java 异常信息返回
    SQL中只要用到聚合函数就一定要用到group by 吗?
    ASP.NET使用一般处理程序实现上传文本文件后实时读取
    git-删除远程不存在的分支
    Spring Boot-整合Retry框架重试机制
    vue3 Unsupported URL Type “npm:“: npm:vue-loader@^16.0.0-beta.7
    npm : 无法加载文件 D: odejs pm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 http://go.microsoft.com/fwlink
    flutter dio请求DioError [DioErrorType.DEFAULT]: SocketException: Insecure socket connections are disallowed by platform: ****
    Vue Element UI el-table 样式属性重叠发生错位
  • 原文地址:https://www.cnblogs.com/huxuhong/p/12911986.html
Copyright © 2011-2022 走看看