ソースコード学習のためのMyBatisの根底にあるクエリ原理

ガイド付き読書

この記事では、MyBatis の低バージョンのバグ (バージョン 3.4.5 より前) から始めて、MyBatis の完全なクエリ プロセスを分析し、構成ファイルの解析からクエリの完全な実行プロセスまで、MyBatis のクエリ プロセスを詳細に解釈します。 . MyBatis のワンタイム クエリ プロセスの詳細については、こちらを参照してください。通常のコード記述では、MyBatis の下位バージョン (3.4.5 より前のバージョン) でバグが発見されました. 多くのプロジェクトのバージョンは 3.4.5 よりも古いため、ここでは簡単な例で再現します.ソースコードの観点から MyBatis クエリのプロセスを分析し、誰もが MyBatis のクエリ原理を理解できるようにします。

 

1 問題現象

1.1 シナリオ問題再現

次の図に示すように、Mapper の例では、student テーブルからクエリ条件を満たすデータをクエリするために、以下に queryStudents メソッドが提供されています. 入力パラメータは、student_name または Student_name のコレクションにすることができます. 例では、パラメータStudentName の List のみを渡します。

 List<String> studentNames = new LinkedList<>();
 studentNames.add("lct");
 studentNames.add("lct2");
 condition.setStudentNames(studentNames);

 

  <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">


        select * from student
        <where>
            <if test="studentNames != null and studentNames.size > 0 ">
                AND student_name IN
                <foreach collection="studentNames" item="studentName" open="(" separator="," close=")">
                    #{studentName, jdbcType=VARCHAR}
                </foreach>
            </if>


            <if test="studentName != null and studentName != '' ">
                AND student_name = #{studentName, jdbcType=VARCHAR}
            </if>
        </where>
    </select>

実行の期待される結果は

select * from student WHERE student_name IN ( 'lct' , 'lct2' )

しかし、実際に走った結果は

==> 準備: select * from student WHERE 学生名 IN ( ? , ? ) AND 学生名 = ?

==> パラメーター: lct(文字列)、lct2(文字列)、lct2(文字列)

<== 列: id、student_name、age

<== 行: 2、lct2、2

<== 合計: 1

実行結果からわかるように、student_name には別の値が割り当てられていませんが、MyBatis の解析後、student_name だけに値が割り当てられています.MyBatis は SQL の解析と値の割り当てに問題があると推測できます.変数. 最初の推測は foreach ループです. foreach 内の変数の値が foreach の外側に持ち出されるため、SQL 解析で例外が発生します.

2 MyBatis クエリの原則

2.1 MyBatis アーキテクチャ

2.1.1 アーキテクチャ図

MyBatis の全体的なアーキテクチャ モデルを簡単に見てみましょう. 全体として、MyBatis は主に 4 つのモジュールに分かれています:

インターフェース層:主な機能はデータベースを扱うことです

データ処理層: データ処理層は MyBatis の中核とも言えるもので、次の 2 つの機能を備えています。

  • パラメーターを渡して動的 SQL ステートメントを作成します。
  • List<E> を統合するための SQL ステートメントの実行とクエリ結果のカプセル化

フレームワーク サポート レイヤー: 主に、トランザクション管理、接続プール管理、キャッシング メカニズム、および SQL ステートメントの構成が含まれます。

ブート層: ブート層は、MyBatis 構成情報を構成および開始する方法です。MyBatis は、MyBatis をガイドする 2 つの方法を提供します: XML 構成ファイルベースと Java API ベース

2.1.2 4 つの MyBatis オブジェクト

MyBatis、ParameterHandler、ResultSetHandler、StatementHandler、および Executor のフレームワーク全体を実行する 4 つのコア オブジェクトがあります. 4 つのオブジェクトは、フレームワーク全体の実行プロセスを実行します. 4 つのオブジェクトの主な機能は次のとおりです。

  • ParameterHandler: プリコンパイル済みパラメーターの設定
  • ResultSetHandler: 返された SQL の結果セットを処理します
  • StatementHandler: SQL ステートメントのプリコンパイル、パラメーターの設定、およびその他の関連作業を処理します。
  • Executor: 追加、削除、変更、およびクエリ操作を実行するために使用される、MyBatis のエグゼキュータ

2.2 ソースコードからMyBatisのクエリ処理を解釈する

最初に、問題を再現するコードと対応する準備プロセスを示します

2.2.1 データの準備

CREATE TABLE `student`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `student_name` varchar(255) NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1;


-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, 'lct', 1);
INSERT INTO `student` VALUES (2, 'lct2', 2);

 

2.2.2 コードの準備

1.マッパー構成ファイル

<?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" >


<mapper namespace="mybatis.StudentDao">
    <!-- 映射关系 -->
    <resultMap id="resultMap" type="mybatis.Student">
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="student_name" property="studentName" jdbcType="VARCHAR" />
        <result column="age" property="age" jdbcType="INTEGER" />


    </resultMap>


    <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">


        select * from student
        <where>
            <if test="studentNames != null and studentNames.size > 0 ">
                AND student_name IN
                <foreach collection="studentNames" item="studentName" open="(" separator="," close=")">
                    #{studentName, jdbcType=VARCHAR}
                </foreach>
            </if>


            <if test="studentName != null and studentName != '' ">
                AND student_name = #{studentName, jdbcType=VARCHAR}
            </if>
        </where>
    </select>


</mapper>

2. サンプルコード

public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //1.获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //2.获取对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //3.获取接口的代理类对象
        StudentDao mapper = sqlSession.getMapper(StudentDao.class);
        StudentCondition condition = new StudentCondition();
        List<String> studentNames = new LinkedList<>();
        studentNames.add("lct");
        studentNames.add("lct2");
        condition.setStudentNames(studentNames);
        //执行方法
        List<Student> students = mapper.queryStudents(condition);
    }

 

2.2.3 クエリ プロセスの分析

1. SqlSessionFactory の構築

まず、SqlSessionFactory オブジェクトの作成プロセスを見てください。

//1.获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

コードは、まず SqlSessionFactoryBuilder でビルド メソッドを呼び出してオブジェクトを取得し、ビルド メソッドに入ります。

 public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

独自のビルド メソッドを呼び出す

図 1 ビルド メソッド自体がデバッグの凡例を呼び出す

このメソッドでは、受信した MyBatis 構成ファイルを解析するために XMLConfigBuilder オブジェクトが作成され、解析のために parse メソッドが呼び出されます。

図 2 parse 解析入力パラメーターのデバッグの凡例

このメソッドでは、xml のコンテンツは、MyBatis の構成ファイルのルート ディレクトリから取得されます. パーサー オブジェクトは、xml ファイルを解析するために特別に使用される XPathParser オブジェクトです. xml ファイルから各ノードを取得する方法? いいえさらなる説明がここに与えられます。ここでは、構成ファイルの解析が構成ノードから開始されていることがわかります。これは、MyBatis 構成ファイルのルート ノードでもあります。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>


    <properties>
        <property name="dialect" value="MYSQL" />  <!-- SQL方言 -->
    </properties>

次に、解析された xml ファイルを parseConfiguration メソッドに渡します。このメソッドでは、構成ファイル内の各ノードの構成が取得されます。

図 3 構成のデバッグの凡例の解析

マッパー ノードの構成を取得して特定の解析プロセスを確認するには

 <mappers>
        <mapper resource="mappers/StudentMapper.xml"/>
    </mappers>

mapperElement メソッドに入る

mapperElement(root.evalNode("mappers"));

図 4 mapperElement メソッドのデバッグの凡例

parse メソッドで XMLMapperBuilder オブジェクトを作成することにより、MyBatis が依然としてマッパー ノードを解析していることを確認します。

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }


  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

configurationElement メソッドを呼び出して構成された各マッパー ファイルを解析します。

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"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

マッパーでCRUDタグを解析してマッパーファイルを解析する方法を見てみましょう

buildStatementFromContext メソッドに入る

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

MyBatis は、XMLStatementBuilder オブジェクトを作成することにより、ノードの追加、削除、変更、およびクエリ ノードを引き続き解析していることがわかります。このオブジェクトの parseStatementNode メソッドを呼び出すことにより、このタグの下に構成されたすべての構成情報がこのメソッドで取得され、設定されます。

図 5 parseStatementNode メソッドのデバッグの凡例

解析が完了したら、addMappedStatement メソッドを使用してすべての構成を MappedStatement に追加し、マップされたステートメントを構成に追加します。

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
    resultSetTypeEnum, flushCache, useCache, resultOrdered, 
    keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

 

マップされたステートメントに CRUD タグの詳細が含まれていることがわかります

図 7 Mappedstatement オブジェクト メソッドのデバッグの凡例

また、構成には、mapperRegistertry や mappedStatements を含むすべての構成情報が含まれます。

図 8 Config オブジェクト メソッドのデバッグの凡例

特定のプロセス

図 9 SqlSessionFactory オブジェクトの構築プロセス図 9 SqlSessionFactory オブジェクトの構築プロセス

2. SqlSession 作成プロセス

SqlSessionFactory が作成されたら、SqlSession の作成プロセスを見てみましょう。

SqlSession sqlSession = sqlSessionFactory.openSession();

まず、DefaultSqlSessionFactory の openSessionFromDataSource メソッドが呼び出されます。

@Override
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

このメソッドでは、最初に DataSource などのプロパティが構成から取得されてオブジェクト Environment が形成され、トランザクション オブジェクト TransactionFactory が Environment のプロパティを使用して構築されます。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    final Environment environment = configuration.getEnvironment();
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    final Executor executor = configuration.newExecutor(tx, execType);
    return new DefaultSqlSession(configuration, executor, autoCommit);
  } catch (Exception e) {
    closeTransaction(tx); // may have fetched a connection so lets call close()
    throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

トランザクションが作成された後、Executor オブジェクトが作成されます。Executor オブジェクトの作成は、executorType に基づいています。デフォルトは SIMPLE タイプです。構成がない場合、SimpleExecutor が作成されます。2 番目のレベルのキャッシュが有効な場合、 CachingExecutor が作成されます。

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
  executorType = executorType == null ? defaultExecutorType : executorType;
  executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
  Executor executor;
  if (ExecutorType.BATCH == executorType) {
    executor = new BatchExecutor(this, transaction);
  } else if (ExecutorType.REUSE == executorType) {
    executor = new ReuseExecutor(this, transaction);
  } else {
    executor = new SimpleExecutor(this, transaction);
  }
  if (cacheEnabled) {
    executor = new CachingExecutor(executor);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

After the executor is created, the executor = (Executor)
interceptorChain.pluginAll(executor) method will be execute. このメソッドの対応する意味は、各インターセプターを使用してエグゼキューターをラップして返し、最後に DefaultSqlSession メソッドを呼び出してSqlSession

図 10 SqlSession オブジェクトの作成プロセス

3.Mapperの取得手順

SqlSessionFactory と SqlSession では、対応する Mapper を取得し、Mapper でメソッドを実行する必要があります。

StudentDao mapper = sqlSession.getMapper(StudentDao.class);

最初のステップでは、すべてのマッパーが MapperRegistry オブジェクトに配置されていることがわかっているため
、org.apache.ibatis.binding.MapperRegistry#getMapper メソッドを呼び出して、対応するマッパーを取得します。

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

MyBatisではすべてのマッパーがプロキシクラスに対応しており、マッパーに対応するプロキシクラスを取得した後、newInstanceメソッドを実行して対応するインスタンスを取得し、このインスタンスを介してメソッドを呼び出せるようにします。

public class MapperProxyFactory<T> {


  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();


  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }


  public Class<T> getMapperInterface() {
    return mapperInterface;
  }


  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }


  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }


  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }


}

マッパーを取得するプロセスは

図 11 Mapper の取得プロセス

4. クエリ プロセス

マッパーを取得したら、特定のメソッドを呼び出すことができます

//执行方法
List<Student> students = mapper.queryStudents(condition);

最初
に org.apache.ibatis.binding.MapperProxy#invoke のメソッドが呼び出されます. このメソッドでは org.apache.ibatis.binding.MapperMethod#execute が呼び出されます.

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
   Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    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 {
        Object param = method.convertArgsToSqlCommandParam(args);
        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;
}

まず、SQLの追加、削除、変更、チェックの種類に応じて、どのメソッドを実行するかを決定します.ここでは、SELECTメソッドを実行します.SELECTでは、メソッドの戻り値の型に応じて、どのメソッドを実行するかを決定します. select には selectone 用の個別のメソッドはありませんが、selectList が使用されていることがわかり
ます。 .Object) メソッド

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    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();
  }
}

selectList では、最初に構成オブジェクトから MappedStatement を取得します。この MappedStatement には、ステートメント内の Mapper の関連情報が含まれており、
org.apache.ibatis.executor.CachingExecutor#query() メソッドを呼び出します。

図 12 query() メソッドのデバッグ図

この方法では、最初に SQL が解析され、入力パラメーターと元の SQL に従って SQL が結合されます。

図 13 SQL スプライシング プロセスのコード図

MapperedStatement で getBoundSql を呼び出して最終的に解析される SQL は、

図 14 SQL スプライシング プロセスの結果の図

次に、
org.apache.ibatis.parsing.GenericTokenParser#parse を呼び出して、解析済みの SQL を解析します

図 15 SQL 解析プロセスの図

最終的な分析結果は

図 16 SQL 解析結果の図

最後に、SimpleExecutor の doQuery メソッドが呼び出され、このメソッドで StatementHandler が取得され、
メソッド org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize が呼び出されてパラメーターと SQL が処理され、最後にステートメントの execute メソッドが呼び出されて結果セットが取得され、resultHandler を使用してノットが処理されます。

図 17 SQL 処理結果の図

クエリの主なプロセスは次のとおりです。

 

図 18 クエリ フロー処理図

5. クエリ プロセスの概要

クエリ プロセス全体は次のように要約されます。

図 19 クエリ プロセスの抽象化

2.3 シナリオの問題の原因と解決策

2.3.1 個人調査

このバグが発生する場所は、SQL パラメーターをバインドするときに、ソース コード内の場所が

 @Override
 public <E> List<E> 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);
}

記述された SQL は動的にバインドされたパラメーターを持つ SQL であるため、最終的に
メソッド org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSqlに移動します。

public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> 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;
}

このメソッドでは、rootSqlNode.apply(context) メソッドが呼び出されます.このタグは foreach タグであるため
、メソッド org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply に対してapply メソッドが呼び出されます。

@Override
public boolean apply(DynamicContext context) {
  Map<String, Object> bindings = context.getBindings();
  final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
  if (!iterable.iterator().hasNext()) {
    return true;
  }
  boolean first = true;
  applyOpen(context);
  int i = 0;
  for (Object o : iterable) {
    DynamicContext oldContext = context;
    if (first) {
      context = new PrefixedContext(context, "");
    } else if (separator != null) {
      context = new PrefixedContext(context, separator);
    } else {
        context = new PrefixedContext(context, "");
    }
    int uniqueNumber = context.getUniqueNumber();
    // Issue #709 
    if (o instanceof Map.Entry) {
      @SuppressWarnings("unchecked") 
      Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
      applyIndex(context, mapEntry.getKey(), uniqueNumber);
      applyItem(context, mapEntry.getValue(), uniqueNumber);
    } else {
      applyIndex(context, i, uniqueNumber);
      applyItem(context, o, uniqueNumber);
    }
    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
    if (first) {
      first = !((PrefixedContext) context).isPrefixApplied();
    }
    context = oldContext;
    i++;
  }
  applyClose(context);
  return true;
}

appItm メソッドが呼び出されると、パラメーターがバインドされ、パラメーターの変数の問題はバインディングのパラメーター領域に存在します。

private void applyItem(DynamicContext context, Object o, int i) {
  if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
  }
}

パラメーターをバインドする場合、foreach メソッドをバインドすると、foreach の 2 つのパラメーターがバインディングでバインドされているだけでなく、追加のパラメーター名 studentName->lct2 もバインドされていることがわかります。これは、最後のパラメーターも次のようになることを意味します。バインディング パラメータ、

private void applyItem(DynamicContext context, Object o, int i) {
  if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
  }
}

 

図 20 パラメータ バインド プロセス

最終的な判断

org.apache.ibatis.scripting.xmltags.IfSqlNode#apply

@Override
public boolean apply(DynamicContext context) {
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    contents.apply(context);
    return true;
  }
  return false;
}

evaluateBoolean メソッドが呼び出されると、context.getBindings() が上記のバインディング パラメーターであり、渡されることがわかります。これは、このパラメーターに StudentName が含まれているためです。Ognl 式を使用すると、次のように決定されます。タグが価値がある場合、このタグは解析されます

図 21 単一パラメータ バインド プロセス

最終的なバインディング結果は

図 22 すべてのパラメータ バインディング プロセス

したがって、この場所のバインディング パラメータに問題があり、これまでに問題が見つかっています。

2.3.2 公式説明

検証のためにMyBatisの公式ドキュメントを読んだところ、バージョン3.4.5のリリースでのバグ修正にそのような文があることがわかりました

図 23 この問題は github レコードで公式に修正されています 図 23 この問題は github レコードで公式に修正されています

foreach版でのグローバル変数のコンテキスト変更のバグを修正

問題のアドレスは https://github.com/mybatis/mybatis-3/pull/966 です

修正は https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a です

 

foreach がグローバル変数を変更するという問題を解決するために、オブジェクトをグローバル変数とローカル変数をそれぞれ格納するように再定義する、公式の修正計画を確認できます。

図 24 この問題の公式修理コードの例

2.3.3 修理計画

  • MyBatis のバージョンを 3.4.5 以上にアップグレードする
  • バージョンが変更されていない場合、foreach で定義された変数名は外部のものと一致しないはずです

3 ソースコード読み込み処理のまとめ

MyBatis ソース コードのディレクトリは比較的明確で、基本的に同じ機能を持つすべてのモジュールがまとめられていますが、ソース コードを直接読むと、その実行プロセスを理解するのはまだ難しい場合があります。最初から最後まで、MyBatis で使用されているデザイン パターンなど、MyBatis の設計と処理の流れを確認できます。

図 25 MyBatis コード構造図

  • 組み合わせモード: ChooseSqlNode、IfSqlNode など。
  • テンプレート メソッド パターン: BaseExecutor や SimpleExecutor など、BaseTypeHandler や IntegerTypeHandler などのすべてのサブクラス
  • ビルダー モード: SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder など
  • ファクトリ モード: SqlSessionFactory、ObjectFactory、MapperProxyFactory など
  • プロキシモード: MapperProxy、ConnectionLogger などの MyBatis 実装のコア

4 ドキュメント参照

https://mybatis.org/mybatis-3/en/index.htm

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/4090830/blog/5581667