MyBatisマッパーインターフェースからSQL実行までの処理フロー解析

前回の記事では、mapper.xml内のSQLタグに対応するMappedStatementがどのように初期化されるかを分析しました。また、Mapperインターフェースがどのようにロードされるかも既に解析しています。ここでの疑問は、これらがそれぞれConfigurationにロードされているとして、実際の使用時にMappedStatementとロードされたmapperインターフェースがどのように関連付けられるのかということです。この記事でその解析を行います。

まず、SqlSessionインターフェースのメソッドについて説明します。つまり

<T> T getMapper(Class<T> type);

明らかにこのメソッドはクラス名によってそのクラスのインスタンスを取得するものであり、Mapperインターフェースは実装クラスを持たずインターフェースのみ定義されているため、返されるのはmapper.xmlに基づいて生成されたインスタンスであると考えられます。具体的にはどのように実現されているのでしょうか。まずはこのメソッドの実装を見てみましょう。

DefaultSqlSessionがこのメソッドを実装したコードは以下の通りです:

1 @Override
2   public <T> T retrieveMapper(Class<T> targetType) {
3     return configuration.<T>retrieveMapper(targetType, this);
4   }

このメソッドは非常にシンプルで、ConfigurationオブジェクトのretrieveMapperメソッドを呼び出しています。次にConfiguration内部での実装を見てみます。コードは以下の通りです:

1 public <T> T retrieveMapper(Class<T> targetType, SqlSession session) {
2     return registry.retrieveMapper(targetType, session);
3   }

registryのretrieveMapperメソッドを呼び出し、パラメータはClassオブジェクトとsessionです。次にMapperRegistryの実装を見てみましょう。コードは以下の通りです:

 1  @SuppressWarnings("unchecked")
 2     public <T> T retrieveMapper(Class<T> targetType, SqlSession session)
 3     {
 4         // knownMappersからMapperProxyFactoryオブジェクトを取得
 5         final MapperProxyFactory<T> factory = (MapperProxyFactory<T>)registeredMappers.get(targetType);
 6         if (factory == null)
 7         {
 8             throw new BindingException("Type " + targetType + " is not registered in the MapperRegistry.");
 9         }
10         try
11         {
12             // プロキシファクトリを通じて新しいインスタンスを作成
13             return factory.createInstance(session);
14         }
15         catch (Exception e)
16         {
17             throw new BindingException("Error obtaining mapper instance. Cause: " + e, e);
18         }
19     }

targetTypeに基づいてregisteredMappersコレクションから該当mapperのプロキシファクトリクラスを取得し、そのプロキシファクトリを使って新しいインスタンスを作成していることがわかります。次にプロキシファクトリがどのようにインスタンスを作成するかを見てみましょう。コードは以下の通りです:

 1  @SuppressWarnings("unchecked")
 2     protected T createNewInstance(MapperProxy<T> proxy) {
 3         // プロキシを通じてmapperインターフェースの新しいインスタンスを取得
 4       return (T) Proxy.newProxyInstance(mapperType.getClassLoader(), new Class[] { mapperType }, proxy);
 5     }
 6 
 7     public T createInstance(SqlSession session) {
 8       // mapperプロキシオブジェクトを作成し、createNewInstanceメソッドを呼び出す
 9       final MapperProxy<T> proxy = new MapperProxy<T>(session, mapperType, cache);
10       return createNewInstance(proxy);
11     }

これで、Mapper.classに基づいてMapperインターフェースのインスタンスを取得する方法がわかりましたが、現時点ではまだMappedStatementとの関連は見えていません。しかし、心配しないでください。次にプロキシによって生成されたmapperインスタンスが具体的なメソッドを実行する際にどのように処理されるかを見てみましょう。コードは以下の通りです:

 1 @Override
 2     public Object handleInvocation(Object proxy, Method invokedMethod, Object[] parameters) throws Throwable {
 3       // 実行されるメソッドが親クラスObjectクラスのメソッド(toStringやhashCodeなど)かどうかを判定
 4       // もしそうであればリフレクションで直接実行し、Objectのメソッドでなければさらに進む
 5       // もしこの判定がない場合どうなるでしょうか?
 6       // mapperインターフェースは定義されたインターフェースメソッドだけでなくObjectから継承したメソッドも含むため、
 7       // 判定がないと引き続き進んでしまい、以下の実行プロセスではmapper.xmlから対応する実装メソッドを探すが、
 8       // mapper.xmlはmapper内のインターフェースメソッドのみ実装しており、toStringやhashCodeメソッドがないため、
 9       // これらのメソッドが実装できないことになる
10       if (Object.class.equals(invokedMethod.getDeclaringClass())) {
11         try {
12           return invokedMethod.invoke(this, parameters);
13         } catch (Throwable t) {
14           throw ExceptionUtil.unwrapThrowable(t);
15         }
16       }
17       final MapperMethod targetMethod = getCachedMapperMethod(invokedMethod);
18       // mapperMethodオブジェクトのexecuteメソッドを実行
19       return targetMethod.execute(sqlSession, parameters);
20     }
21 
22     private MapperMethod getCachedMapperMethod(Method method) {
23       // キャッシュからmethodオブジェクトに基づいてMapperMethodオブジェクトを取得
24       MapperMethod cachedMethod = cache.get(method);
25       if (cachedMethod == null) {
26         // cachedMethodがnullの場合、新しいMapperMethodを作成
27         cachedMethod = new MapperMethod(mapperType, method, sqlSession.getConfiguration());
28         cache.put(method, cachedMethod);
29       }
30       return cachedMethod;
31     }

プロキシがmapperインターフェースのメソッドを実行する際、まずMapperMethodオブジェクトを作成し、その後executeメソッドを実行することがわかります。コードは以下の通りです:

 1 public Object execute(SqlSession session, Object[] args) {
 2         Object executionResult;
 3         if (SqlCommandType.INSERT == command.getType()) {// INSERTコマンドを実行
 4           Object parameter = method.convertArgsToSqlCommandParam(args);// パラメータ構築
 5           executionResult = rowCountResult(session.insert(command.getName(), parameter));// sqlSessionのinsertメソッドを呼び出し
 6         } else if (SqlCommandType.UPDATE == command.getType()) {// UPDATEコマンドを実行
 7           Object parameter = method.convertArgsToSqlCommandParam(args);// パラメータ構築
 8           executionResult = rowCountResult(session.update(command.getName(), parameter));// sqlSessionのupdateメソッドを呼び出し
 9         } else if (SqlCommandType.DELETE == command.getType()) {// DELETEコマンドを実行
10           Object parameter = method.convertArgsToSqlCommandParam(args);// パラメータ構築
11           executionResult = rowCountResult(session.delete(command.getName(), parameter));// sqlSessionのdeleteメソッドを呼び出し
12         } else if (SqlCommandType.SELECT == command.getType()) {// SELECTコマンドを実行
13           if (method.returnsVoid() && method.hasResultHandler()) {// インターフェースの戻り値型を判定し、データ型に応じて対応するSELECT文を実行
14             executeWithResultHandler(session, args);
15             executionResult = null;
16           } else if (method.returnsMany()) {
17             executionResult = executeForMany(session, args);
18           } else if (method.returnsMap()) {
19             executionResult = executeForMap(session, args);
20           } else if (method.returnsCursor()) {
21             executionResult = executeForCursor(session, args);
22           } else {
23             Object parameter = method.convertArgsToSqlCommandParam(args);
24             executionResult = session.selectOne(command.getName(), parameter);
25           }
26         } else if (SqlCommandType.FLUSH == command.getType()) {
27             executionResult = session.flushStatements();
28         } else {
29           throw new BindingException("Unknown execution method for: " + command.getName());
30         }
31         if (executionResult == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
32           throw new BindingException("Mapper method '" + command.getName() 
33               + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
34         }
35         return executionResult;
36       }

実行プロセスの大まかな流れは、MapperMethodのメソッドタイプに基づき、対応するパラメータを構築し、その後sqlSessionのメソッドを実行することであることがわかります。現時点でもまだMappedStatementの姿は見えませんので、MapperMethodがどのように作成されるかを見てみましょう。

1   private final SqlCommand command;
2   private final MethodSignature signature;
3 
4   public MapperMethod(Class<?> interfaceType, Method method, Configuration config) {
5     this.command = new SqlCommand(config, interfaceType, method);
6     this.signature = new MethodSignature(config, interfaceType, method);
7   }

ここではSqlCommandとMethodSignatureの2つのクラスが関係しています。SqlCommandから見てみましょう。

 1 public SqlCommand(Configuration configuration, Class<?> interfaceType, Method method) {
 2         String statementId = interfaceType.getName() + "." + method.getName();// インターフェースメソッド名
 3         MappedStatement statement = null;
 4         if (configuration.hasStatement(statementId)) {
 5             // configurationからインターフェース名に基づいてMappedStatementオブジェクトを取得
 6           statement = configuration.getMappedStatement(statementId);
 7         } else if (!interfaceType.equals(method.getDeclaringClass())) { // 問題 #35
 8             // このメソッドがこのmapperインターフェースのメソッドでない場合、mapperの親クラスから該当インターフェースに対応するMappedStatementオブジェクトを探す
 9           String parentStatementId = method.getDeclaringClass().getName() + "." + method.getName();
10           if (configuration.hasStatement(parentStatementId)) {
11             statement = configuration.getMappedStatement(parentStatementId);
12           }
13         }
14         if (statement == null) {
15           if(method.getAnnotation(Flush.class) != null){
16             identifier = null;
17             type = SqlCommandType.FLUSH;
18           } else {
19             throw new BindingException("Invalid bound statement (not found): " + statementId);
20           }
21         } else {
22           identifier = statement.getId();// identifierをMappedStatementのIDに設定、IDの値はXMLに対応するSQL文
23           type = statement.getSqlCommandType();// typeをMappedStatementのSQLタイプに設定
24           if (type == SqlCommandType.UNKNOWN) {
25             throw new BindingException("Unknown execution method for: " + identifier);
26           }
27         }
28       }

ようやくMappedStatementの姿が見えてきました。mapperのClassオブジェクトとmethodオブジェクトに基づいてConfigurationオブジェクトから指定されたMappedStatementオブジェクトを取得し、MappedStatementオブジェクトの値に基づいてSqlCommandオブジェクトのプロパティを初期化しています。一方、MethodSignatureはSQL文のシグネチャであり、主な役割はSQLパラメータと戻り値の型を判定することです。

まとめ:

1. sqlSessionがconfigurationオブジェクトのgetMapperメソッドを呼び出し、configurationがmapperRegistryのgetMapperメソッドを呼び出す

2. mapperRegistryがmapperに基づいて対応するMapperプロキシファクトリを取得する

3. mapperプロキシファクトリを通じてmapperのプロキシを作成する

4. mapperメソッドを実行する際、プロキシ呼び出しを通じて該当mapperメソッドのMapperMethodオブジェクトを作成する

5. MapperMethodオブジェクトの作成は、configurationオブジェクトから指定されたMappedStatementオブジェクトを取得することで具体的なSQL文およびパラメータと戻り値の型を取得する

6. sqlSessionに対応するinsert、update、delete、selectメソッドを呼び出してmapperのメソッドを実行する

以上で、mapperインターフェースとmapper.xmlがどのように関連付けられるか、mapperインターフェースがどのようにインスタンスを取得するか、mapperメソッドが最終的にSqlSessionのメソッドを呼び出すことがわかりました。では、SqlSessionは各SQLメソッドを具体的にどのように実行するのでしょうか?次の記事でさらに解析していきます......

タグ: MyBatis Mapper SQL configuration proxy

7月17日 16:50 投稿