深入浅出Mybatisソースコード解析——BoundSql取得プロセス

11000 ワード

前言
更に1ヶ月余り止まって、博主はずっと技术の学习と仕事の忙しい间に忙しくて、実はもっと多いのは技术の中で迷って、しかしやはりMybatisシリーズを引き続き更新したいと思っています.博主も自分に20年にいくつかのflagを立てました:Javaを深く勉強し、c++を研究し、3つ目はフィットネスです.2019年を振り返ると、技術学習の道をあまり歩いていないと感じているので、今回は自分に厳しくしなければならないと思っています.
余談ですが、前の文章を振り返ってみると、久しぶりだったので少し覚えていません.前の記事で簡単にMybatisがMapperエージェントオブジェクトを取得するプロセスについて説明しましたが、mapperのエージェントオブジェクトを取得するとSQLSessionの実行プロセスになります.しかし、私たちがMybatisを学ぶときは、実行プロセスの1つを理解しているだけで、内部のコード原理を理解していません.では、MybatisのBoundSql取得プロセスを見てみましょう.
一、getBoundSql
SQLSessionのプロセスを実行すると、SQL文の実行プロセスに入りますが、この時点で私たちのSQLは実行可能な文ではありません.パラメータのバインドを経て本格的な実行を行う必要があります.前の記事では、getBoundSQLの世代コードエントリについて説明しました.次のようにします.
	@Override
	public  List selectList(String statement, Object parameter, RowBounds rowBounds) {
		try {
			//      statementId,  MappedStatement  
			MappedStatement ms = configuration.getMappedStatement(statement);
			//           
			// RowBounds       (                 ,        )
			// wrapCollection(parameter)             
			return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
		} catch (Exception e) {
			throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
		} finally {
			ErrorContext.instance().reset();
		}
	}

上のコードからgetBoundSQLコードの文字は見えないようですが、前述したようにgetBoundSQLはqueryが実行する前に行われています.では、上のqueryメソッドを見てみましょう.Ctrを押してマウスをクリックすると、エディタはBaseExecutorクラスとCachingExecutorクラスの2つの実装を提示します.しかし、この2つの中のどのクラスでも、最後に付いてきたコードはMappedStatementというクラスにあります.では、ここではBaseExecutorのコードを例に挙げます.コードは以下の通りです.
  @Override
  public  List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

上のコードではgetBoundSqlという方法が見られましたが、ここではgetBoundSqlのコアコードではないので、続けてみましょう.コードは次のとおりです.
  public BoundSql getBoundSql(Object parameterObject) {
	// 
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }

上記のコードの論理は、まずparameterObjectパラメータによってboundSqlオブジェクトを取得し、boundSqlによってparameterMappingsという集合を取得し、取得した集合nullまたは空の場合、newのBoundSqlオブジェクトは、取得されたものであれ作成されたものであれ、最後に遍歴するプロセスを行う.遍歴の過程において、rmIdを介してconfigurationオブジェクトにResultMapオブジェクトを取得する必要がある、取得可能であればこのような演算を行う.hasNestedResultMaps()、|=演算子について、興味のある方は2人で復習してみてください.実はどこだ?hasNestedResultMaps()が書いたバイナリはhasNestedResultMapsのバイナリと1つの演算を行います.
ここで言うポイントはgetBoundSqlですので、引き続きフォローしてみましょう.Ctrを押してマウスをクリックすると、D y n amicSqlSource、ProviderSqlSource、RawSqlSource、StaticSqlSource、VelocitySqlSourceといういくつかのクラスで実装されている方法がエディタに提示されます.これらのクラスではProviderSqlSourceは廃止されています.しかし、彼らはすべてSqlSourceというクラスを実現しています.StaticSqlSourceのgetBoundSqlは結局newのBoundSqlオブジェクトです.DynamicSqlSourceとVelocitySqlSourceクラスの実装は少し複雑です.ここでは、DynamicSqlSourceというクラスの実装を例に挙げます.コードは以下の通りです.
public class DynamicSqlSource implements SqlSource {

	private final Configuration configuration;
	private final SqlNode rootSqlNode;

	public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) {
		this.configuration = configuration;
		this.rootSqlNode = rootSqlNode;
	}

	@Override
	public BoundSql getBoundSql(Object parameterObject) {
		DynamicContext context = new DynamicContext(configuration, parameterObject);
		rootSqlNode.apply(context);
		//   SQL     
		SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
		//       
		Class> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
		//     :   #{} SQL      ,     StaticSqlSource 
		SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
		//      SQL           (        ,           SQL   ?)
		BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
		for (Map.Entry entry : context.getBindings().entrySet()) {
			boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
		}
		return boundSql;
	}

}

上のコードではまずconfigurationオブジェクトと入力パラメータparameterObjectによってDynamicContextオブジェクトを作成し、このコンテキストオブジェクトをrootSqlNodeに渡します.実はSqlNodeノードです.このノードクラスでは主にSQL文のif、foreachなどのラベルのようなノードが使用されます.コンテキストオブジェクトをrootSqlNodeに渡した後、configurationオブジェクトによってSqlSourceBuilderオブジェクトを作成します.このオブジェクトはSQL情報解析器で、主に#{}を持つSQL文を解析し、StaticSqlSourceにカプセル化し、最後にsqlSourceでBoundSqlを取得し、BoundSqlを遍歴してパラメータ割り当てを行います.最後にboundSqlオブジェクトを返します.
実は上のコードの中で、その主なコアコードはparse方法で、次にこの方法を見てみましょう.
二、解析を実行する
	public SqlSource parse(String originalSql, Class> parameterType, Map additionalParameters) {
		ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
				additionalParameters);
		//        
		GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
		//   #{}
		String sql = parser.parse(originalSql);
		//       SQL  ,   StaticSqlSource   
		return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
	}

コードは簡単です.大体の手順は次のとおりです.
  • ParameterMappingTokenHandler
  • を作成する
  • GenericTokenParser
  • を作成する
  • 次いでoriginalSql
  • を解析する
  • 最後に解析後のSQL情報をStaticSqlSourceオブジェクトにカプセル化する
  • 1).ParameterMappingTokenHandler
    ParameterMappingTokenHandlerは、BaseBuilderを継承し、TokenHandlerを実装する内部クラスです.構造関数を簡単に見てみましょう.
    public ParameterMappingTokenHandler(Configuration configuration, Class> parameterType,
    		Map additionalParameters) {
    	super(configuration);
    	this.parameterType = parameterType;
    	this.metaParameters = configuration.newMetaObject(additionalParameters);
    }

    このコンストラクション関数では親クラスのメソッドを呼び出し,configurationによってnewMetaObjectを作成した.
    2).GenericTokenParser
    GenericTokenParserというクラスは主に分詞解析器であり,その構造関数には解析対象のopenTokenとcloseTokenを指定し,プロセッサを指定する.
      public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
        this.openToken = openToken;
        this.closeToken = closeToken;
        this.handler = handler;
      }

    3).解析#{}または${}
    解析#{}コードの複雑さについては、コードを直接見ます.
      /**
       *   ${} #{}
       * @param text
       * @return
       */
      public String parse(String text) {
        if (text == null || text.isEmpty()) {
          return "";
        }
        // search open token
        int start = text.indexOf(openToken, 0);
        if (start == -1) {
          return text;
        }
        char[] src = text.toCharArray();
        int offset = 0;
        final StringBuilder builder = new StringBuilder();
        StringBuilder expression = null;
        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 {
            // found open token. let's search close token.
            if (expression == null) {
              expression = new StringBuilder();
            } else {
              expression.setLength(0);
            }
            builder.append(src, offset, start - offset);
            offset = start + openToken.length();
            int end = text.indexOf(closeToken, offset);
            while (end > -1) {
              if (end > offset && src[end - 1] == '\\') {
                // this close token is escaped. remove the backslash and continue.
                expression.append(src, offset, end - offset - 1).append(closeToken);
                offset = end + closeToken.length();
                end = text.indexOf(closeToken, offset);
              } else {
                expression.append(src, offset, end - offset);
                offset = end + closeToken.length();
                break;
              }
            }
            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();
      }
    }

    このコードは主に${}と#{}を解析するもので、具体的な詳細は子供靴たちが自分でデバッグするときに細かく味わうことができますが、実はここでこの文章も終わります.この文章は主にgetBoundSqlと解析を実行することを話しています.ちなみにRawSqlSourceのクラス実装を補完します.
     
    三、RawSqlSource
    public class RawSqlSource implements SqlSource {
    
    	private final SqlSource sqlSource;
    
    	public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class> parameterType) {
    		this(configuration, getSql(configuration, rootSqlNode), parameterType);
    	}
    
    	public RawSqlSource(Configuration configuration, String sql, Class> parameterType) {
    		//   SQL  
    		SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    		//       
    		Class> clazz = parameterType == null ? Object.class : parameterType;
    		//     
    		sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap());
    	}
    
    	private static String getSql(Configuration configuration, SqlNode rootSqlNode) {
    		DynamicContext context = new DynamicContext(configuration, null);
    		rootSqlNode.apply(context);
    		return context.getSql();
    	}
    
    	@Override
    	public BoundSql getBoundSql(Object parameterObject) {
    		return sqlSource.getBoundSql(parameterObject);
    	}
    
    }

    実はその原理は大同小異で、ただ異なる需要の実現方式が少し驚いているからだ.
     
    次に、このコード実行プロセスを簡単にまとめます.
  • DynamicSqlSource#getBoundSql
  • SqlSourceBuilder#parse:SQL文の#{}を解析し、対応するパラメータ情報をParameterMappingオブジェクトセットにカプセル化し、StaticSqlSourceにカプセル化
  • ParameterMappingTokenHandler#構築方法
  • GenericTokenParser#構築方法:分析対象のopenTokenとcloseTokenを指定し、プロセッサ
  • を指定します.
  • GenericTokenParser#parse:SQL文を解析し、openTokenとcloseTokenの内容を処理します.
  • ParameterMappingTokenHandler#handleToken:tokenの処理(#{}/${})
  • ParameterMappingTokenHandler#buildParameterMapping:ParameterMappingオブジェクトを作成する

  • StaticSqlSource#構築方法:解析後のSQL情報を、StaticSqlSource
  • にカプセル化する

  • RawSqlSource#getBoundSql
  • StaticSqlSource#getBoundSql
  • BoundSql#構築方法:解析後のsql情報、パラメータマッピング情報、インパラメータオブジェクトをBoundSqlオブジェクト中の
  • に組み合わせる.