MyBatis クエリ実行プロセスの内部動作解析:SimpleExecutor の doQuery() メソッド

ステートメントハンドラーのルーティングと生成

クエリ処理の起点である SimpleExecutor.doQuery() は、SQL の準備から実行結果のオブジェクト変換までを一貫して管理する。最初のフェーズでは、Configuration インスタンスを通じて適切な StatementHandler を構築する。この過程は RoutingStatementHandler によって中継され、MappedStatement に定義されたステートメント種別に応じて具体的な実装クラスへ委譲する。

// MappedStatement の定義に基づいたハンドラー生成
StatementType targetType = mappedStatement.getStatementType();
StatementHandler routedHandler = switch (targetType) {
    case STATEMENT -> new SimpleStatementHandlerWrapper(executor, mappedStmt, paramObj, bounds, resHandler, boundSql);
    case CALLABLE -> new CallableStatementHandlerWrapper(executor, mappedStmt, paramObj, bounds, resHandler, boundSql);
    default -> new PreparedStatementHandlerWrapper(executor, mappedStmt, paramObj, bounds, resHandler, boundSql);
};

デフォルト設定では PREPARED が選択されるため、プリコンパイル機能を持つ実装が採用される。このハンドラー内部には、SQL パラメータのバインドを担当するコンポーネントと、取得したデータベース結果を Java オブジェクト群へ変換するコンポーネントが紐付けられている。

内部コンポーネントの初期化とプラグイン介入ポイント

BaseStatementHandler の基底構造では、設定クラスからパラメータ処理系と結果セット処理系を生成し、内部フィールドに保持する。これにより、SQL 構築とデータ変換の責務が分離される。

// コンポーネント初期化メソッドの再構成
protected void initializeInternalComponents(Executor execInstance, MappedStatement ms, 
                                            Object targetParam, RowBounds rBounds, 
                                            ResultHandler rh, BoundSql bSql) {
    ParameterHandler paramProc = configInstance.buildParameterHandler(ms, targetParam, bSql);
    ResultSetHandler resProc = configInstance.buildResultSetHandler(execInstance, ms, rBounds,
                                                                    paramProc, rh, bSql);
    this.boundParamHandler = paramProc;
    this.boundResHandler = resProc;
}

生成された三つの主要オブジェクト(StatementHandler, ParameterHandler, ResultSetHandler)は、フレームワークの拡張機構であるインターセプターチェーンに渡される。これにより、動的プロキシが生成され、SQL 実行前後やパラメータ設定時にカスタムロジックを透過的に埋め込むことができる。なお、Executor 自体のプロキシ化はバインディングフェーズで完了しているため、ここでは残余のコンポーネントが対象となる。

// インターセプターチェーンを通じたプロキシ生成
ParameterHandler proxiedParamHandler = (ParameterHandler) interceptorChain.applyAll(rawParamHandler);
ResultSetHandler proxiedResHandler = (ResultSetHandler) interceptorChain.applyAll(rawResHandler);
StatementHandler proxiedStatementHandler = (StatementHandler) interceptorChain.applyAll(routedHandler);

JDBC ステートメントの準備とパラメータバインド

ハンドラー群の準備が整うと、物理的な JDBC ステートメントの確保に進む。接続プールからステートメントを取得し、トランザクションコンテキストやクエリタイムアウト設定を反映する。

// JDBC ステートメントの初期化とキャッシュ制御
Statement jdbcStmt = proxiedStatementHandler.createStatement(connection, transactionTimeout);

ステートメントが取得された直後、パラメータバインド処理が呼び出される。プラグインが適用されている場合、この時点で拡張ロジックが優先的に実行される。その後、実際の値設定処理が PreparedStatement インスタンスに対して適用される。

@Override
public void applyQueryParameters(Statement stmt) throws SQLException {
    if (stmt instanceof PreparedStatement preparedStmt) {
        proxiedParamHandler.setJDBCParameters(preparedStmt);
    }
}

SQL 実行と結果セットのマッピング処理

パラメータが設定されると、データベースへのクエリ送信が実行される。

// JDBC レベルでのクエリ発効
boolean hasResults = jdbcStmt.execute();

実行が完了した段階で、取得した ResultSet は結果セット処理系へ委譲される。MyBatis の標準実装である DefaultResultSetHandlerhandleResultSets() メソッドを通じて変換ロジックを制御する。

@Override
public List<Object> extractResultSetData(Statement stmt) throws SQLException {
    List<ResultMap> configuredMaps = mappedStatement.getResultMaps();
    ResultSetWrapper rsw = buildResultSetWrapper(stmt.getResultSet());
    List<Object> resultCollection = new ArrayList<>();

    int currentMapIndex = 0;
    // 複数結果セットに対応したループ処理
    while (rsw != null && currentMapIndex < configuredMaps.size()) {
        ResultMap activeMap = configuredMaps.get(currentMapIndex);
        transformResultSetIntoObjects(rsw, activeMap, resultCollection, null);
        rsw = fetchSubsequentResultSet(stmt);
        resetResultSetContext();
        currentMapIndex++;
    }
    return resultCollection;
}

この処理では、単一クエリの場合通常一度のループ実行で完了する。複数結果セットが定義されている場合、while ループがステートメントから次の結果セットを取得しつつ反復処理を行う。各 ResultMap の定義に従ってカラム名とプロパティの対応付けが行われ、ネストされた関連情報や集計データも再帰的な構造で展開される。処理の最終段階で、変換済みのオブジェクトリストが呼び出し元へ返却され、クエリサイクルが閉じる。

タグ: MyBatis sql-execution interceptor-pattern resultsetmapping jdbc-bridge

7月29日 04:07 投稿