LoginSignup
1
0

More than 3 years have passed since last update.

MyBatisでMapperに引数を経由せずにオブジェクトを渡す方法

Posted at

記事投稿時点で見つけきらなかったので調べてみました。

やりたかったこと

  • フィーチャートグルの状態で発行するSQLを一部変更したい
  • でも、フィーチャートグルを使うかどうかはマッパー内部に閉じたい(引数にしたくない)

結論だけみたい人へ

サンプルはこちらにあります。
このソースコードの66行目のaddParameterメソッドを書き換えて渡したいオブジェクトを指定すれば引数で渡したのと同様の方法でアクセスできるようになります。

コードの説明

全体像

まずは該当のソースコードを以下に示します。

ソースコード
CustomSqlSessionConfig.java
package mybatis_implicit_parameter.config;

import lombok.experimental.Delegate;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.Map;

@Configuration
public class CustomSqlSessionConfig {
    @Bean
    public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplateExt(sqlSessionFactory);
    }

    static class SqlSessionTemplateExt extends SqlSessionTemplate {
        public SqlSessionTemplateExt(SqlSessionFactory sqlSessionFactory) {
            super(sqlSessionFactory);
        }

        Object invokeParentMethod(Method method, Object[] args) throws Throwable {
            return makeParentMethodHandle(method).bindTo(this).invokeWithArguments(args);
        }

        MethodHandle makeParentMethodHandle(Method method) throws Exception {
            return MethodHandles.lookup().findSpecial(SqlSessionTemplate.class, method.getName(),
                    MethodType.methodType(method.getReturnType(), method.getParameterTypes()), SqlSessionTemplateExt.class);
        }

        @Delegate
        final SqlSession proxy = (SqlSession)Proxy.newProxyInstance(SqlSessionTemplateExt.class.getClassLoader(),
                new Class[]{SqlSession.class}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if(hasParameter(method)) {
                            args[1] = addParameter(args[1]);
                        }
                        return SqlSessionTemplateExt.this.invokeParentMethod(method, args);
                    }

                    boolean hasParameter(Method method) {
                        Parameter[] params = method.getParameters();
                        return (params.length >= 2 && params[1].getType().equals(Object.class));
                    }

                    Object addParameter(Object parameter) {
                        parameter = (parameter == null) ? new MapperMethod.ParamMap() : parameter;
                        if (parameter instanceof Map) {
                            return addParameter((Map) parameter);
                        } else {
                            throw new RuntimeException("想定してないパターンここに来るなら教えて〜:" + parameter.getClass().getName());
                        }
                    }

                    Object addParameter(Map map) {
                        //暗黙のパラメーターはここでセットする
                        map.put("insertValue", "foo");
                        map.put("updateValue", "bar");
                        return map;
                    }
                });
    }
}

やってることをかいつまんで説明すると、、、

MyBatisは SqlSessionクラス のinsert/select/update/deleteという名の第1引数にString、第2引数にObjectを持つメソッドを経由してMapperの内容を解析してSQLを発行します。
CustomSqlSessionConfigでは、SQLSessionTemplateを拡張してそれぞれのメソッドをオーバーライドして引数経由で渡すのが不適切だと思われるオブジェクトを第2引数のパラメーター(実体はMap)に追加して実行するようにしています。

こだわりポイント:lombokの@Delegateとダイナミックプロキシを組み合わせる

このコードは自己満足的なコードで本当はもっとシンプルに実現できます。
上に書いたことを愚直に実装しようとすればこのようなコードではなくても、↓のように対象のメソッドをオーバーライドすれば実現できます。

愚直なコード
CustomSqlSessionConfig.java
(省略)
@Override 
public int insert(String statement,Object parameter) {
    return super.insert(statement, addParameter(parameter));
}
(省略)

でも、同じことを何度も書くのが嫌だってのでちょっと拘ってみました。

ダイナミックプロキシは、Javaの古いバージョンから実装されてる機能で、引数で指定されたinterface(群)のメソッドを持つオブジェクトを動的に作成し、指定したInvocationHandlerに処理を集約することができる機能です。

一方lombokの@Delegateはアノテートされたフィールドの全てのpublicなメソッドをそのフィールドを保持しているクラスのメソッドとして展開するアノテーションです(細かく調整もできます)。ぱっとイメージが付きにくいかも知れませが、このサイトを見ていただけるとイメージがつき易いかと思います。

この2つを組み合わせると、自身のクラスの任意のメソッド(今回はinsertだったりselectだったり)をひとまとめに処理する事が出来るんです。
ちょっと黒魔術的なコードにはなりがちですが、好きな人は溜まらなく好きなはず!知らんけどw

この方法の欠点は、InvocationHandlerから親クラスの同一メソッドを呼び出す(super.xxx的な呼び出し)のが困難になる点です。InvocationHandlerはMethodオブジェクトを引数として受け取るので、method.invoke(...)と出来ると良いのですが、@Delegateと組み合わせると、StackOverflowを引き起こしてしまいます。これは、親クラスを指定して同一のメソッドシグネチャを持つMethodオブジェクトを取得してきても同じ結果になります。これは、Method#invokeのAPIリファレンスに書かれてるとおりメソッド呼び出しをinvokeVirtualで実現しているためです。

If the underlying method is an instance method, it is invoked using dynamic method lookup as documented in The Java Language Specification, section 15.12.4.4; in particular, overriding based on the runtime type of the target object may occur.
(基礎となるメソッドがインスタンス・メソッドの場合、Java言語仕様セクションに記載されている動的メソッド参照を使用して起動され、特に、ターゲット・オブジェクトの実行時のタイプに基づいてオーバーライドされます。)

super.xxxをリフレクションで実現するためには

MethodHandles.lookup().findSpecial(SqlSessionTemplate.class, method.getName(),
    MethodType.methodType(method.getReturnType(), 
    method.getParameterTypes()), SqlSessionTemplateExt.class);

子クラス内で、findSpecialで親クラスのメソッドのMethodHandleを取得する必要があります。
MethodHandleはこれまでのJavaのリフレクションと異なり、どのクラスで実効しているかで結果が異なります。InvocationHandler実装クラス内でこの処理を実行してもエラーが発生します。

あやしいポイント

  • パラメーターを追加するメソッドかどうかの判定は若干怪しい。
  • パラメーターオブジェクトの型がMap以外のケースがあると動かない。Map以外のケース(nullの場合がある事は確認済み)が来ると動作しません。Map以外が来るかはちゃんと追い切れてません。

動作確認済みライブラリバージョン

ライブラリ名 バージョン
Spring-Boot 2.3.4.RELEASE
mybatis-spring-boot-starter 2.1.3
lombok 5.1.0

サンプルコードについて

サンプルコードは好きに使っていただいて構いませんが、その結果においてはいかなる責任も負いませーん。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0