春のシリーズ-MybatisPlus

1はじめに

以前は、Mybatis XMLをサポートする方法で作成されていましたが、MybaitsPlusはMybatisの拡張バージョンであり、XMLファイルの内容は省略されています。後者は面倒なSQLコンテンツの書き込みを大幅に削減しますが、同様に、複雑なSQLシナリオの場合、フローのようなSQL生成はXMLを記述するのに直感的ではありません。

2.機能

  • 侵入なし:変更を加えずに拡張を行うだけです。導入しても既存のプロジェクトには影響しません。シルクのように滑らかです。
  • 低損失:基本的なCURDは起動時に自動的に注入され、パフォーマンスは基本的に損失なし、直接オブジェクト指向の操作
  • 強力なCRUD操作:組み込みの一般的なマッパー、一般的なサービス、わずかな構成で単一のテーブルのほとんどのCRUD操作を実現でき、さまざまな使用要件を満たすためのより強力な条件ビルダー
  • Lambdaフォーム呼び出しのサポート:Lambda式を使用すると、さまざまなクエリ条件を記述できるので便利です。間違ったフィールドを書き込むことを心配する必要はありません。
  • 主キーの自動生成をサポート:最大4つの主キー戦略(分散された一意のIDジェネレーター-シーケンスを含む)をサポートします。これは、主キーの問題を完全に解決するように自由に構成できます。
  • ActiveRecordモードのサポート:ActiveRecordフォーム呼び出しをサポートします。エンティティクラスは、強力なCRUD操作を実行するためにModelクラスを継承するだけで済みます。
  • カスタムグローバル一般操作のサポート:グローバル一般メソッドインジェクションのサポート(一度書き込み、どこでも使用)
  • 組み込みのコードジェネレーター:コードまたはMavenプラグインを使用して、マッパー、モデル、サービス、コントローラーレイヤーコード、サポートテンプレートエンジン、および使用するその他のカスタム構成をすばやく生成します
  • 組み込みのページングプラグイン:MyBatisの物理ページングに基づいているため、開発者は特定の操作を気にする必要がありません。プラグインを構成した後、ページングを作成することは通常のリストクエリと同等です。
  • ページングプラグインは複数のデータベースをサポートします:MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServerおよびその他のデータベースをサポートします
  • 組み込みのパフォーマンス分析プラグイン:Sqlステートメントとその実行時間を出力できます。開発およびテスト中にこの機能を有効にして、遅いクエリをすばやく検出することをお勧めします。
  • 組み込みのグローバルインターセプトプラグイン:テーブル全体の削除および更新操作のインテリジェントな分析とブロックを提供し、誤操作を防ぐためにインターセプトルールをカスタマイズすることもできます

3.Mybatis階層

春のシリーズ-MybatisPlus

4.Mavenの依存関係

  <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.23</version>
        </dependency>

5.プラグインメカニズム

このプラグインは、ページング、ファジークエリ処理の特殊文字などの追加機能をカスタマイズするのに便利です。プラグインを作成するときは、Interceptorインターフェイスを実装するプラグインクラスに加えて、注釈を使用してプラグインのインターセプトポイントをマークする必要もあります。いわゆるインターセプトポイントとは、プラグインがインターセプトできる方法を指します。MyBatisがインターセプトできる方法は次のとおりです。

  • エグゼキュータ:update、query、flushStatements、commit、rollback、getTransaction、close、isClosed
  • ParameterHandler:getParameterObject、setParameters
  • ResultSetHandler:handleResultSets、handleOutputParameters
  • StatementHandler:準備、パラメーター化、バッチ処理、更新、クエリ

5.1原則

(1)構成読み込みプラグイン

com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration:org.apache.ibatis.plugin.Interceptorを実装するカスタムプラグインクラスをロードします。


public MybatisPlusAutoConfiguration(MybatisPlusProperties properties,
                                        ObjectProvider<Interceptor[]> interceptorsProvider,
                                        ObjectProvider<TypeHandler[]> typeHandlersProvider,
                                        ObjectProvider<LanguageDriver[]> languageDriversProvider,
                                        ResourceLoader resourceLoader,
                                        ObjectProvider<DatabaseIdProvider> databaseIdProvider,
                                        ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider,
                                        ObjectProvider<List<MybatisPlusPropertiesCustomizer>> mybatisPlusPropertiesCustomizerProvider,
                                        ApplicationContext applicationContext) {
        this.properties = properties;
        this.interceptors = interceptorsProvider.getIfAvailable();
        this.typeHandlers = typeHandlersProvider.getIfAvailable();
        this.languageDrivers = languageDriversProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
        this.mybatisPlusPropertiesCustomizers = mybatisPlusPropertiesCustomizerProvider.getIfAvailable();
        this.applicationContext = applicationContext;
    }

@Bean
@ConditionalOnMissingBean
 public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  // ...
  if (!ObjectUtils.isEmpty(this.interceptors)) {
     factory.setPlugins(this.interceptors);
  }
  // ...
}

(2)エグゼキュータ

org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSessionFromDataSource


private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // ...
      // 创建Executor
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } 
    catch (Exception e) {...} 
    finally {...}
  }

com.baomidou.mybatisplus.core.MybatisConfiguration#newExecutor


public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        if (useDeprecatedExecutor) {
            executorType = executorType == null ? defaultExecutorType : executorType;
            executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
            Executor executor;
            if (ExecutorType.BATCH == executorType) {
                executor = new MybatisBatchExecutor(this, transaction);
            } else if (ExecutorType.REUSE == executorType) {
                executor = new MybatisReuseExecutor(this, transaction);
            } else {
                executor = new MybatisSimpleExecutor(this, transaction);
            }
            if (cacheEnabled) {
                executor = new MybatisCachingExecutor(executor);
            }
            // 植入插件
            executor = (Executor) interceptorChain.pluginAll(executor);
            return executor;
        }
        return super.newExecutor(transaction, executorType);
    }

org.apache.ibatis.plugin.InterceptorChain#pluginAll

public Object pluginAll(Object target) {
    // 遍历拦截器
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

 ケースプラグイン 


public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

 org.apache.ibatis.plugin.Plugin#wrap


public static Object wrap(Object target, Interceptor interceptor) {
    // 获取@Intercepts注解的信息,每一个@Signature注解圈定一个方法
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    // 获取目标类实现的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      // JDK动态代理,调用invoke方法
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

 org.apache.ibatis.plugin.Plugin#invoke


public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        // 如何方法声明的类包含@Signature声明的方法,则调用拦截器
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

(3)StatementHandler

com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor#doQuery


public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            // 创建StatementHandler
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = prepareStatement(handler, ms.getStatementLog(), false);
            return stmt == null ? Collections.emptyList() : handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }

org.apache.ibatis.session.Configuration#newStatementHandler


public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // 植入插件
    statementHandler = (StatementHandler) 
interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

(4)ParameterHandler

org.apache.ibatis.executor.statement.BaseStatementHandler#BaseStatementHandler


protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // ...
    // 创建ParameterHandler
    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    // 创建ResultSetHandler
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
  }

org.apache.ibatis.session.Configuration#newParameterHandler


public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 植入插件
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
  }

(5)ResultHandler

org.apache.ibatis.session.Configuration#newResultSetHandler


public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    // 植入插件
    resultSetHandler = (ResultSetHandler) 
interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

4.2実際の戦闘

(1)ページングプラグイン

   @Bean
    public PaginationInnerInterceptor paginationInnerInterceptor() {
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        paginationInnerInterceptor.setDbType(DbType.MYSQL);
        return paginationInnerInterceptor;
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(ObjectProvider<List<InnerInterceptor>> innerInterceptorProviders) {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        List<InnerInterceptor> interceptors = innerInterceptorProviders.getIfAvailable();
        if (!CollectionUtils.isEmpty(interceptors)) {
            for (InnerInterceptor innerInterceptor : interceptors) {
                mybatisPlusInterceptor.addInnerInterceptor(innerInterceptor);
            }
        }
        return mybatisPlusInterceptor;
    }

(2)特殊文字を処理するファジークエリ

         @Bean
    public FuzzyQueryInterceptor fuzzyQueryInterceptor() {
        return new FuzzyQueryInterceptor();
    }
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import priv.whh.std.boot.mybatis.plus.util.EscapeUtil;

import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

/**
 * 模糊查询拦截器方法,处理模糊查询中包含特殊字符(_、%、\)
 *
 */
@Intercepts({
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
                RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
public class FuzzyQueryInterceptor implements Interceptor {
    private static final String LIKE = " like ";
    private static final String QUESTION_MARK = "?";
    private static final String LIKE_WITH_QUESTION_MARK = " like ?";
    private static final String UNDERLINE = "_";
    private static final String PERCENT = "%";
    private static final String EW = "ew";
    private static final String EW_PARAM_NAME_VALUE_PAIRS = "ew.paramNameValuePairs.";
    private static final String DOUBLE_SLASH = "\\";
    private static final String DOUBLE_SLASH_WITH_SPOT = "\\.";
    private static final String DOUBLE_SLASH_WITH_QUESTION_MARK = "\\?";

    @Override
    public Object intercept(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
        // 拦截sql
        Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        Object parameterObject = args[1];
        BoundSql boundSql = statement.getBoundSql(parameterObject);
        String sql = boundSql.getSql();
        // 处理特殊字符
        Object param = modifyLikeSql(sql, parameterObject, boundSql);
        args[1] = param;
        // 返回
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    @SuppressWarnings("unchecked")
    public static Object modifyLikeSql(String sql, Object parameterObject, BoundSql boundSql) {
        boolean paramJudge = parameterObject instanceof HashMap || parameterObject instanceof String;
        if (!paramJudge) {
            return parameterObject;
        }
        if (!sql.toLowerCase().contains(LIKE) || !sql.toLowerCase().contains(QUESTION_MARK)) {
            return parameterObject;
        }
        // 获取关键字的个数(去重)
        String[] strList = sql.split(DOUBLE_SLASH_WITH_QUESTION_MARK);
        Set<String> keyNames = new HashSet<>();
        for (int i = 0; i < strList.length; i++) {
            if (strList[i].toLowerCase().contains(LIKE)) {
                String keyName = boundSql.getParameterMappings().get(i).getProperty();
                keyNames.add(keyName);
            }
        }
        // 对关键字进行特殊字符“清洗”,如果有特殊字符的,在特殊字符前添加转义字符(\)
        for (String keyName : keyNames) {
            HashMap<String, Object> parameter;
            if (parameterObject instanceof HashMap) {
                parameter = (HashMap<String, Object>) parameterObject;
            } else {
                parameter = new HashMap<>(2);
                parameter.put(keyName, parameterObject);
            }
            if (keyName.contains(EW_PARAM_NAME_VALUE_PAIRS) && sql.toLowerCase().contains(LIKE_WITH_QUESTION_MARK)) {
                // 第一种情况:在业务层进行条件构造产生的模糊查询关键字
                QueryWrapper<Object> wrapper = (QueryWrapper<Object>) parameter.get(EW);
                parameter = (HashMap<String, Object>) wrapper.getParamNameValuePairs();

                String[] keyList = keyName.split(DOUBLE_SLASH_WITH_SPOT);
                // ew.paramNameValuePairs.MPGENVAL1,截取字符串之后,获取第三个,即为参数名
                Object a = parameter.get(keyList[2]);
                boolean judge = a instanceof String && (a.toString().contains(UNDERLINE)
                        || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT));
                if (judge) {
                    parameter.put(keyList[2],
                            PERCENT + EscapeUtil.escapeChar(a.toString().substring(1, a.toString().length() - 1)) + PERCENT);
                }
            } else if (!keyName.contains(EW_PARAM_NAME_VALUE_PAIRS) && sql.toLowerCase().contains(LIKE_WITH_QUESTION_MARK)) {
                // 第二种情况:未使用条件构造器,但是在service层进行了查询关键字与模糊查询符`%`手动拼接
                Object a = parameter.get(keyName);
                boolean judge = a instanceof String && (a.toString().contains(UNDERLINE)
                        || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT));
                if (judge) {
                    parameter.put(keyName,
                            PERCENT + EscapeUtil.escapeChar(a.toString().substring(1, a.toString().length() - 1)) + PERCENT);
                }
            } else {
                // 第三种情况:在Mapper类的注解SQL或者XML中进行了模糊查询的拼接
                Object a = parameter.get(keyName);
                boolean judge = a instanceof String && (a.toString().contains(UNDERLINE)
                        || a.toString().contains(DOUBLE_SLASH) || a.toString().contains(PERCENT));
                if (judge) {
                    parameter.put(keyName, EscapeUtil.escapeChar(a.toString()));
                    if (parameterObject instanceof String) {
                        parameterObject = EscapeUtil.escapeChar(a.toString());
                    }
                }
            }
        }
        return parameterObject;
    }
}

6.キャッシュメカニズム

7.セッションメカニズム

9.構成

10.よくある質問

Q:MybatisはSQLインジェクションをどのように防止しますか?

A:Mybatisが#{?}を使用する場合、sqlはプリコンパイルされます(a =?のt_userから選択)。$ {?}が使用される場合、sqlはプリコンパイルされません( a = 1のt_userから選択)。この場合、SQLインジェクションのリスクがあります。テスト後、SQLインジェクションは2つのSQLステートメントをインジェクトでき​​ず、エラーが報告されます。(select * from t_user where a = 1 or 1 = 1)のようなもののみを挿入できます。挿入される変数は、「 '1' or 1 = 1」です。
春のシリーズ-MybatisPlus

Q:構成方法に加えて、QueryWrapはブレークポイントを介してアセンブルされたSQLを表示できますか?

A:いいえ

Q:データベースのタイムスタンプを受け取るためにDateまたはLocalDateTimeを使用する方が良いですか?

Q:継承されたBaseMapper <User>ファイルで、MybatisPlusはPersonなどのカスタムオブジェクトリターンをサポートしていますか?

Q:Mybatis-plus selectOne関数は、1つのデータの戻りをどのように制限しますか?

11.終了

これでこの記事は終わりです。最後の友達に会ってくれてありがとう。最後にみんなが見えます。出発する前に親指を立ててください。何か問題があれば訂正してください。

おすすめ

転載: blog.51cto.com/14969174/2542687