java-mybatis-custom interceptor interceptor-getネイティブSQL補完パブリックパラメータ

java-mybatis-custom interceptor interceptor-getネイティブSQL補完パブリックパラメータ

環境 pgsql + jdk1.8 + mybatis + springboot

I.はじめに

データは PGSQL に保存されます。ビジネス シナリオの拡大により、固定スキーマの使用ではビジネス ストレージ要件を満たすことができません。コード ロジックを変更して、元々コード内にハードコーディングされていたスキーマ名を変更することが急務です。動的変数形式。
最も簡単な方法は、mySchema へのすべての参照を ${schameName} に置き換えてから、schemaName を渡すようにメソッド呼び出しを変更することです。

--原查询脚本
select * from mySchema.myTable;
--替换后的形式
select * from ${mySchema}.myTable;

${mySchema} プレースホルダーが使用される理由は、連結されたテーブル名をパラメータ化してパラメーターを渡すことができないためです。
変更が必要な位置をグローバルに検索して、その数を確認します。

--查询结果为:
Find in Files 300+ matches in 30+ files

一致するファイルを分析した結果、参照場所の一部は XML 内にあり、一部は @Select、@Update などの注釈内にあることがわかりました。思い切ってあきらめてください。
mybatis のインターセプタを使用することを考えました。呼び出しメソッドの仮パラメータを変更する必要がなく、非侵襲的な変更を使用してパラメータ インジェクションを完了できます。まず、インターセプタの基本情報を確認します

2、4 台の mybatis インターセプター

Mybatis は 4 つの傍受可能なオブジェクトを提供します。

  • Executor SQL の内部エグゼキュータ
  • ParameterHandler はパラメータの処理をインターセプトします
  • StatementHandler が SQL の構築をインターセプトする
  • ResultSetHandlerインターセプト結果処理

これら 4 つの異なるタイプのインターセプタのインターセプトの順序は上から下です:
Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler
Executor タイプのインターセプタなど、同じタイプの 2 つのインターセプタがある場合、実行順序はインターセプタを逆の順序で SqlSessionFactory に追加します。
たとえば、Executor タイプ A インターセプタが SqlSessionFactory に追加され、Executor タイプ B インターセプタが追加された場合、B インターセプタが最初に実行され、次に A インターセプタが実行されます。

インターセプタを宣言する

  • @Intercepts アノテーションと @Signature アノテーションを使用してインターセプターを宣言する
@Component
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SchemaParamsterInterceptor implements Interceptor {
}
  • @Signature アノテーションパラメータの説明
    • type インターセプト可能な 4 つのクラスのクラス オブジェクト。
    • メソッド クラスのメソッド名。上記の prepare は StatementHandler の prepare メソッドです。
    • args メソッドの仮パラメータの型である量の型は、インターセプト クラスのメソッド宣言と一致します。

レジスタインターセプタ

  • @Configuration を使用して注釈を構成する
@Configuration
public class MybatisConfig {
    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;
    /**
     * mybatis 拦截器注册
     */
    @PostConstruct
    public void addSqlInterceptor() {
        SchemaParamsterInterceptor interceptor = new SchemaParamsterInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }
}

3. 動的パラメータ注入スキーム:

オプション 1: 通常の SQL プレースホルダー ${...} を使用する

このアイデアは、SQL 定義で標準のプレースホルダー ${schemaName} を使用し、インターセプターの元の SQL に ${schemaName} 文字が出現するかどうかを判断し、出現する場合は schemaName パラメーターを挿入することです。
利点: 標準呼び出しと互換性があり、開発者は schemaName を渡すかどうかを選択でき、渡されない場合は自動的に挿入でき、渡されたパラメータは渡されたパラメータを表示するために使用されます。

  • ParameterHandler インターセプターの使用時に発生した問題:
    • ${schemaName} プレースホルダーは SQL 定義に書き込まれていますが、渡されません。これは Executor インターセプターでは異常であり、ParameterHandler インターセプターにはまったく到達できません。
    • 次に、Executor インターセプターを使用してみます。
  • Executor インターセプターの使用時に発生する問題:
    • 受信パラメータの型が固定されておらず (マップ、ポジョ、基本型)、判断して挿入するのが不便であるため、テストは失敗しました。
@Component
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SchemaParamsterInterceptor implements Interceptor {

    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 拦截 Executor 的 query 方法 生成sql前将 任意参数 设置到实体中
        if (invocation.getTarget() instanceof Executor ) {
            //&& "query".equals(invocation.getMethod().getName())
            return invokeQuery(invocation);
        }
        return null;
    }

    /** 获取原始sql */
    private String getRawSQL(Invocation invocation) throws NoSuchFieldException, IllegalAccessException {
        //反射获取 SqlSource 对象,通过此对象获取原始SQL
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        /** SqlSource{@link org.apache.ibatis.mapping.SqlSource}的实现类比较多,不方便在所有实现类中解析原始SQL*/
        SqlSource sqlSource = ms.getSqlSource();
        //通过MappedStatement.SqlSource对象获取原生sql不太靠谱!!!
        if(sqlSource instanceof DynamicSqlSource) {
            DynamicSqlSource dynamicSqlSource = (DynamicSqlSource) ms.getSqlSource();
            if (dynamicSqlSource == null)
                return null;
            //反射获取 TextSqlNode 对象
            Field sqlNodeField = dynamicSqlSource.getClass().getDeclaredField("rootSqlNode");
            sqlNodeField.setAccessible(true);
            TextSqlNode rootSqlNode = (TextSqlNode) sqlNodeField.get(dynamicSqlSource);
            //反射获取原生sql
            Field textField = rootSqlNode.getClass().getDeclaredField("text");
            textField.setAccessible(true);
            String sql = String.valueOf(textField.get(rootSqlNode));
            return sql;
        }
        if(sqlSource instanceof RawSqlSource) {
            RawSqlSource rawSqlSource = (RawSqlSource) ms.getSqlSource();
            if (rawSqlSource == null)
                return null;
            //反射获取 TextSqlNode 对象
            Field sqlSourceField = rawSqlSource.getClass().getDeclaredField("sqlSource");
            sqlSourceField.setAccessible(true);
            StaticSqlSource staticSqlSource = (StaticSqlSource) sqlSourceField.get(rawSqlSource);
            //反射获取原生sql
            Field sqlField = staticSqlSource.getClass().getDeclaredField("sql");
            sqlField.setAccessible(true);
            String sql = String.valueOf(sqlField.get(staticSqlSource));
            return sql;
        }
        return null;
    }

    private Object invokeQuery(Invocation invocation) throws Exception {
        //todo 按需添加注入參數提高性能
//        String sql = getRawSQL(invocation);
//        if(StringUtils.isBlank(sql) || sql.indexOf(schemaParamsPlaceholder)==-1)
//            return null;

        Executor executor = (Executor) invocation.getTarget();

        // 获取第一个参数
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        // mybatis的参数对象
        Object paramObj = invocation.getArgs()[1];
        if (paramObj == null) {
            MapperMethod.ParamMap<Object> param = new MapperMethod.ParamMap<>();
            paramObj = param;
        }
        //执行脚本
        processParam(paramObj);
        RowBounds rowBounds = (RowBounds)invocation.getArgs()[2];
        ResultHandler resultHandler  = (ResultHandler)invocation.getArgs()[3];
        return executor.query(ms, paramObj,rowBounds,resultHandler);
    }
    /** 处理参数对象 */
    private void processParam(Object parameterObject) throws IllegalAccessException, InvocationTargetException {
        //如果是map且map的key中没有需要的参数,则添加到参数map中
        if (parameterObject instanceof Map) {
            String schemaName = "mySchema";
            ((Map) parameterObject).putIfAbsent("schemaName", schemaName);
            return;
        }
    }
}

オプション 2: 非標準のプレースホルダーを使用する

アイデア: カスタム プレースホルダーを使用すると、パラメーター定義の検証をスキップし、SQL の前処理中にプレースホルダーを正しいスキーマ名に置き換えることができます。利点: 注釈によって定義された XML および SQL との互換性があり、select、select、sql などのすべての Type sql をサポートします
。挿入、削除、更新;
欠点: 受信パラメータの表示に互換性がないため、実行されたすべての SQL がインターセプトされます。

/***
 * schema参数动态注入
 */
@Log4j
@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SchemaParamsterInterceptor implements Interceptor {
    /** SQL中的占位符 */
    private static final String schemaPlaceholder = "_schemaName";
    

    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //
        if(invocation.getTarget() instanceof  StatementHandler){
            if("prepare".equals(invocation.getMethod().getName()))
                return invokeStatementHandlerPrepare(invocation);
        }
        return null;
    }

    private Object invokeStatementHandlerPrepare(Invocation invocation) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InvocationTargetException {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        log.debug("prepare~~~~~~~~~~~~~~~begin");
        System.out.println(sql);
        if(StringUtils.isNotEmpty(sql) && sql.indexOf(schemaPlaceholder)>-1){
            String adminSchema = "mySchema";
            sql = sql.replaceAll(schemaPlaceholder,adminSchema);
            //通过反射回写
            Field sqlNodeField = boundSql.getClass().getDeclaredField("sql");
            sqlNodeField.setAccessible(true);
            sqlNodeField.set(boundSql,sql);
            log.debug("prepare~~~~~~~~~~~~~~~replace");
            log.debug(sql);
        }
        log.debug("prepare~~~~~~~~~~~~~~~end");


        return invocation.proceed();
    }
}

4. 結論

2 番目のスキームを使用すると、スキーマの動的な置換の要求が完全に解決されます。
プロジェクトの [ファイルで置換] を使用して、元の固定スキーマ名を _schemaName プレースホルダーに置き換えます。
コードは完璧に機能します!

おすすめ

転載: blog.csdn.net/xxj_jing/article/details/127905738