Mybatis 适配多数据源 关键字查询问题

Mybatis 适配多数据源 关键字查询问题

前言

最近项目使用Mybatis拦截器对国产化数据源数据进行关键字处理,以下记录如何将拦截器集成到项目中以及在使用过程中踩过的一些小坑

1.Myabtis拦截器是什么?

MyBatis允许使用者在映射语句执行过程中的某一些指定的节点进行拦截调用,通过织入拦截器,在不同节点修改一些执行过程中的关键属性,从而影响SQL的生成、执行和返回结果,如:来影响Mapper.xml到SQL语句的生成、执行SQL前对预编译的SQL执行参数的修改、SQL执行后返回结果到Mapper接口方法返参POJO对象的类型转换和封装等。

2,Mybatis核心对象

从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:

Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。
SqlSessionFactory:SqlSession工厂。
SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
Executor:MyBatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
StatementHandler:MyBatis直接在数据库执行SQL脚本的对象。另外它也实现了MyBatis的一级缓存。
ParameterHandler:负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
MappedStatement:MappedStatement维护了一条<select|update|delete|insert>节点的封装。
SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
BoundSql:表示动态生成的SQL语句以及相应的参数信息。

3,如何使用
3.1 首先介绍一下mybatis拦截器拦截的类型

在这里插入图片描述

自定义拦截器必须使用@Intercepts注解,并实现Interceptor接口

@Intercepts注解:代表该类是一个拦截器

@Signature注解:表示要拦截哪一个方法

同时@Signature注解有三个参数

type:MyBatis拦截器默认可以拦截的类型只有四种,即四种接口类型Executor、StatementHandler、ParameterHandler和ResultSetHandler,type可以选取四种类型中的一个
method:对应接口中的方法,比图Executor的query方法
args:对应某个接口方法中的参数
3.2 代码示例

代码中使用的mybatis-plus的版本

            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.4.3</version>
            </dependency>

拦截器类

@Component
@Intercepts({@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(
                type = Executor.class,
                method = "update",
                args = {MappedStatement.class, Object.class}
        )})
public class KeywordInterceptor implements Interceptor {


    public DataSourceProperties getDataSourceProperties() {
        return SpringContextUtil.getBean(DataSourceProperties.class);
    }

    private static final Logger logger = LoggerFactory.getLogger(KeywordInterceptor.class);
    public static final String DATABASE_TYPE_DM = "dm";

    public static final String DATABASE_TYPE_KINGBASE = "kingbase";

    public static final String DATABASE_TYPE_GBASE = "gbase";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        logger.info("Intercepted target class: {}", invocation.getTarget().getClass().getName());
        if (invocation.getTarget() instanceof Executor) {
            MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
            Object parameter = invocation.getArgs()[1];
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            String originalSql = boundSql.getSql();
            System.out.println("Original SQL: " + originalSql);
            if (containsKeyword(originalSql, "关键字") || containsKeyword(originalSql, "关键字")) {
                String databaseId = mappedStatement.getDatabaseId();
                String currentDataSourceType;
                if (StringUtils.isNotEmpty(databaseId)) {
                    currentDataSourceType = getCurrentDataSourceType(databaseId);
                } else {
                    DataSourceProperties dataSourceProperties = getDataSourceProperties();
                    String driverClassName = dataSourceProperties.getDriverClassName();
                    currentDataSourceType = getCurrentDataSourceType(driverClassName);
                }
                SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
                String modifiedSql = modifySqlForDataSource(originalSql, currentDataSourceType, sqlCommandType);
                logger.info("Modified SQL: {}", modifiedSql);
                // 修改 SQL 语句(注意,这里是简化示例,实际情况可能需要更多处理)
                BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), modifiedSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
                // 复制其他字段
                copyBoundSqlFields(boundSql, newBoundSql);
                // 创建新的 MappedStatement 对象
                MappedStatement newMappedStatement = newMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
                // 替换原始的 MappedStatement 对象
                invocation.getArgs()[0] = newMappedStatement;

                return invocation.proceed();
            } else {
                return invocation.proceed();
            }
            
        } else {
            return invocation.proceed();
        }
    }

    private void copyBoundSqlFields(BoundSql source, BoundSql target) throws IllegalAccessException {
        Field[] fields = BoundSql.class.getDeclaredFields();
        for (Field field : fields) {
            if (!field.getName().equals("sql")) {
                field.setAccessible(true);
                field.set(target, field.get(source));
            }
        }
    }


    private boolean containsKeyword(String sql, String keyword) {
        // 使用正则表达式检查 SQL 中是否包含关键字
        Matcher matcher = Pattern.compile("\\b" + keyword + "\\b", Pattern.CASE_INSENSITIVE).matcher(sql);
        return matcher.find();
    }


    private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (null != ms.getKeyProperties()) {
            builder.keyProperty(String.join(",", ms.getKeyProperties()));
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    private static class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

    private String getCurrentDataSourceType(String driverClassName) {
        if (StringUtils.contains(driverClassName, DATABASE_TYPE_KINGBASE)) {
            return DbType.KINGBASE_ES.getDb();
        } else if (StringUtils.contains(driverClassName, DATABASE_TYPE_DM)) {
            return DbType.DM.getDb();
        } else if (StringUtils.contains(driverClassName, DATABASE_TYPE_GBASE)) {
            return DbType.GBASE.getDb();
        } else {
            return DbType.MYSQL.getDb();
        }
    }

    private String modifySqlForDataSource(String sql, String dataSourceType, SqlCommandType sqlCommandType){
        if ("mysql".equalsIgnoreCase(dataSourceType)) {
            return sql.replaceAll("\\b关键字\\b", "`关键字`");
        } else {
            if ("kingbasees".equalsIgnoreCase(dataSourceType)) {
                return sql.replaceAll("\\b关键字\\b", "\"关键字\"");
            } else {
                if ("dm".equals(dataSourceType)) {
                    return sql.replaceAll("\\b关键字\\b", "\"关键字大写\"");
                } else {
                    return sql.replaceAll("\\b关键字\\b", "\"关键字\"");
                }
            }
        }
    }

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

    @Override
    public void setProperties(Properties properties) {
        // 可以通过配置文件传递参数
    }
}

DataProperties类

@Component
@ConfigurationProperties(
        prefix = "spring.datasource"
)
public class DataSourceProperties {
    private String driverClassName;
    private String url;
    private String username;
    private String password;

    // Getters and Setters
    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }
}

Mybatis自动配置类

@Configuration
@EnableAutoConfiguration
@MapperScan(value = "扫的类路径", annotationClass = Mapper.class, sqlSessionFactoryRef = "sqlSessionFactory")
@MapperScan(value = "扫的类路径", annotationClass = Repository.class, sqlSessionFactoryRef = "sqlSessionFactory")
public class DGMybatisAutoConfiguration {

    @Autowired
    private DataSourceProperties dataSourceProperties;

    @Resource
    private MultiTenantHandler multiTenantHandler;

    public static final String DATABASE_TYPE_MYSQL = "mysql";

    public static final String DATABASE_TYPE_DM = "dm";

    public static final String DATABASE_TYPE_KINGBASE = "kingbase";

    public static final String DATABASE_TYPE_GBASE = "gbase";

    @Bean(destroyMethod = "")
    public DruidDataSource dataSource() {

        DruidDataSource druidDataSource = new DruidDataSource();

        druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        druidDataSource.setUrl(dataSourceProperties.getUrl());
        druidDataSource.setUsername(dataSourceProperties.getUsername());
        druidDataSource.setPassword(dataSourceProperties.getPassword());

        druidDataSource.setPoolPreparedStatements(true);
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTestOnBorrow(true);
        druidDataSource.setTestOnReturn(true);
        druidDataSource.setKeepAlive(true);
        druidDataSource.setMinIdle(5);
        druidDataSource.setMaxActive(50);
        druidDataSource.setMaxWait(60000);
        druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
        druidDataSource.setInitialSize(5);
        druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
        druidDataSource.setTimeBetweenConnectErrorMillis(60000);
        druidDataSource.setMinEvictableIdleTimeMillis(300000);
        druidDataSource.setValidationQueryTimeout(3);
        //auto commit
        druidDataSource.setDefaultAutoCommit(true);
        return druidDataSource;
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new TenantLineInnerInterceptor(multiTenantHandler));
        String driverClassName = dataSourceProperties.getDriverClassName();
        // 分页插件
        if (StringUtils.contains(driverClassName, DATABASE_TYPE_GBASE)) {
            mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.GBASE));
        } else if (StringUtils.contains(driverClassName, DATABASE_TYPE_DM)) {
            mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.DM));
        } else if (StringUtils.contains(driverClassName, DATABASE_TYPE_KINGBASE)) {
            mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.KINGBASE_ES));
        } else {
            mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        }
        return mybatisPlusInterceptor;
    }


    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setLogImpl(org.apache.ibatis.logging.stdout.StdOutImpl.class);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        configuration.setCallSettersOnNulls(true);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.addInterceptor(mybatisPlusInterceptor());
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setConfiguration(configuration);
        sqlSessionFactoryBean.setDataSource(dataSource());

        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        dbConfig.setIdType(IdType.AUTO);
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setDbConfig(dbConfig);
        sqlSessionFactoryBean.setPlugins(new KeywordInterceptor());
        return sqlSessionFactoryBean.getObject();
    }
}
4. 总结

当然也可以使用Mybatis拦截器来做其他事情,比如数据加密脱敏,sql监控报警等功能,这也会额外产生一些性能开销,合理利用拦截器将会大大缩减开发成本。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值