深入浅出Mybatisソースコード解析——BoundSql取得プロセス
11000 ワード
前言
更に1ヶ月余り止まって、博主はずっと技术の学习と仕事の忙しい间に忙しくて、実はもっと多いのは技术の中で迷って、しかしやはりMybatisシリーズを引き続き更新したいと思っています.博主も自分に20年にいくつかのflagを立てました:Javaを深く勉強し、c++を研究し、3つ目はフィットネスです.2019年を振り返ると、技術学習の道をあまり歩いていないと感じているので、今回は自分に厳しくしなければならないと思っています.
余談ですが、前の文章を振り返ってみると、久しぶりだったので少し覚えていません.前の記事で簡単にMybatisがMapperエージェントオブジェクトを取得するプロセスについて説明しましたが、mapperのエージェントオブジェクトを取得するとSQLSessionの実行プロセスになります.しかし、私たちがMybatisを学ぶときは、実行プロセスの1つを理解しているだけで、内部のコード原理を理解していません.では、MybatisのBoundSql取得プロセスを見てみましょう.
一、getBoundSql
SQLSessionのプロセスを実行すると、SQL文の実行プロセスに入りますが、この時点で私たちのSQLは実行可能な文ではありません.パラメータのバインドを経て本格的な実行を行う必要があります.前の記事では、getBoundSQLの世代コードエントリについて説明しました.次のようにします.
上のコードからgetBoundSQLコードの文字は見えないようですが、前述したようにgetBoundSQLはqueryが実行する前に行われています.では、上のqueryメソッドを見てみましょう.Ctrを押してマウスをクリックすると、エディタはBaseExecutorクラスとCachingExecutorクラスの2つの実装を提示します.しかし、この2つの中のどのクラスでも、最後に付いてきたコードはMappedStatementというクラスにあります.では、ここではBaseExecutorのコードを例に挙げます.コードは以下の通りです.
上のコードではgetBoundSqlという方法が見られましたが、ここではgetBoundSqlのコアコードではないので、続けてみましょう.コードは次のとおりです.
上記のコードの論理は、まず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というクラスの実装を例に挙げます.コードは以下の通りです.
上のコードではまずconfigurationオブジェクトと入力パラメータparameterObjectによってDynamicContextオブジェクトを作成し、このコンテキストオブジェクトをrootSqlNodeに渡します.実はSqlNodeノードです.このノードクラスでは主にSQL文のif、foreachなどのラベルのようなノードが使用されます.コンテキストオブジェクトをrootSqlNodeに渡した後、configurationオブジェクトによってSqlSourceBuilderオブジェクトを作成します.このオブジェクトはSQL情報解析器で、主に#{}を持つSQL文を解析し、StaticSqlSourceにカプセル化し、最後にsqlSourceでBoundSqlを取得し、BoundSqlを遍歴してパラメータ割り当てを行います.最後にboundSqlオブジェクトを返します.
実は上のコードの中で、その主なコアコードはparse方法で、次にこの方法を見てみましょう.
二、解析を実行する
コードは簡単です.大体の手順は次のとおりです. ParameterMappingTokenHandler を作成する GenericTokenParser を作成する次いでoriginalSql を解析する最後に解析後のSQL情報をStaticSqlSourceオブジェクトにカプセル化する 1).ParameterMappingTokenHandler
ParameterMappingTokenHandlerは、BaseBuilderを継承し、TokenHandlerを実装する内部クラスです.構造関数を簡単に見てみましょう.
このコンストラクション関数では親クラスのメソッドを呼び出し,configurationによってnewMetaObjectを作成した.
2).GenericTokenParser
GenericTokenParserというクラスは主に分詞解析器であり,その構造関数には解析対象のopenTokenとcloseTokenを指定し,プロセッサを指定する.
3).解析#{}または${}
解析#{}コードの複雑さについては、コードを直接見ます.
このコードは主に${}と#{}を解析するもので、具体的な詳細は子供靴たちが自分でデバッグするときに細かく味わうことができますが、実はここでこの文章も終わります.この文章は主にgetBoundSqlと解析を実行することを話しています.ちなみにRawSqlSourceのクラス実装を補完します.
三、RawSqlSource
実はその原理は大同小異で、ただ異なる需要の実現方式が少し驚いているからだ.
次に、このコード実行プロセスを簡単にまとめます. 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オブジェクト中の に組み合わせる.
更に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は、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);
}
}
実はその原理は大同小異で、ただ異なる需要の実現方式が少し驚いているからだ.
次に、このコード実行プロセスを簡単にまとめます.