文章目录
- 背景
- 1. 自定义通用方法的实现
- 1.1 新增 Mapper 方法与 SQL 语句脚本映射枚举
- 1.2 新增通用方法的定义类
- 1.3 新增 SQL 注入器
- 1.4 新增配置类将 SQL 注入器添加到容器
- 1.5 新增基类 Mapper
- 2. 实现原理
- 2.1 自定义 SQL 注入器的注入
- 2.2 自定义 SQL 注入器的使用
- 2.3 Mapper 操作数据库的实现
背景
项目中使用了读写分离的数据库访问框架,这个框架基于 Proxy 模式
,对使用方屏蔽了主从数据库,查询时默认路由到从库。但是因为存在主从延迟,当主库数据变更较大时延迟会达到秒级,造成了一些线上问题。中间件同事建议在对主从延迟敏感的场景中绑定主库查询,绑定方式是在 SQL 语句的开头添加特定的注释字符串,访问框架会根据这个字符串执行主库路由。这个场景涉及到 SQL 语句的拦截修改,立即想到了如下两个方案,评估后最终选择了第二个方案
- 基于 MyBatis 的
@Intercepts
拦截器实现,这种方式如果要做到方法级别的拦截修改,需要一些方法标注的额外开发量- 扩展 MyBatis-plus 的通用方法,增加专门的走主库的查询方法
1. 自定义通用方法的实现
MyBatis-plus
提供了许多通用的数据库操作方法,其定义包含在枚举com.baomidou.mybatisplus.core.enums.SqlMethod
中,仿照其实现,自定义一个通用的数据库操作方法的步骤如下
1.1 新增 Mapper 方法与 SQL 语句脚本映射枚举
新增枚举SqlMethodEx
,其内部属性定义与 SqlMethod
完全一致
SqlMethodEx
实际维护了上层Mapper 方法
与底层SQL 语句脚本
的映射,本例中MASTER_SELECT_LIST
定义了一个Mapper#selectListFromMaster()
方法及其对应的 SQL 语句脚本的基本结构,可以看到本例中笔者在 SQL 语句开始处添加了一个前缀字符串
public enum SqlMethodEx {
MASTER_SELECT_LIST("selectListFromMaster", "从主库查询满足条件多条数据",
"<script>%s " + SqlConstant.DA_MASTER_PARAM + "SELECT %s FROM %s %s %s\n</script>"),
;
private final String method;
private final String desc;
private final String sql;
SqlMethodEx(String method, String desc, String sql) {
this.method = method;
this.desc = desc;
this.sql = sql;
}
public String getMethod() {
return method;
}
public String getDesc() {
return desc;
}
public String getSql() {
return sql;
}
}
1.2 新增通用方法的定义类
新增 SelectListFromMasterMethod
类继承于 AbstractMethod
,负责将我们自定义的通用 Mapper方法 及其对应的 SQL 语句脚本组装为 MappedStatement
对象并添加到容器中,对 MappedStatement
对象不了解的读者可参考 MyBatis Mapper 操作数据库源码流程总结
public class SelectListFromMasterMethod extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethodEx sqlMethod = SqlMethodEx.MASTER_SELECT_LIST;
String sqlSelectColumns = sqlSelectColumns(tableInfo, true);
String sqlWhere = sqlWhereEntityWrapper(true, tableInfo);
String sql = String.format(sqlMethod.getSql(), sqlFirst(), sqlSelectColumns, tableInfo.getTableName(), sqlWhere, sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addSelectMappedStatementForTable(mapperClass, getMethod(sqlMethod), sqlSource, tableInfo);
}
private String getMethod(SqlMethodEx sqlMethod) {
return sqlMethod.getMethod();
}
}
1.3 新增 SQL 注入器
新增 MasterSqlInjector
类继承于 DefaultSqlInjector
,负责将所有通用的 Mapper方法 的定义类收集起来,后续将使用这些方法定义类为每一个具体的 Mapper 添加通用方法
public class MasterSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methods = super.getMethodList(mapperClass);
methods.add(new SelectListFromMasterMethod());
return methods;
}
}
1.4 新增配置类将 SQL 注入器添加到容器
MyBatis-plus
的组件注入和普通的 Spring 配置注入完全一致,本例中笔者自定义的 SQL 注入器 MasterSqlInjector
注入到容器后,即相当于提供了 Mapper#selectListFromMaster()
方法的底层 SQL 语句基础,接下来则需要定义上层的 Mapper 方法供使用方调用
@Configuration
public class MybatisPlusConfig {
@Bean
public MasterSqlInjector masterSqlInjector() {
return new MasterSqlInjector();
}
}
1.5 新增基类 Mapper
新增 MasterMapper
继承于 BaseMapper
,并在其中定义一个 selectListFromMaster方法
使用时具体的业务 Mapper 只需要继承
MasterMapper
即可像使用其它MyBatis-plus 提供的全局方法一样使用其定义的MasterMapper#selectListFromMaster()
方法
public interface MasterMapper<T> extends BaseMapper<T> {
/**
* 根据 entity 条件,从主库查询任意条记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<T> selectListFromMaster(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
}
2. 实现原理
2.1 自定义 SQL 注入器的注入
在 MyBatis-plus 源码解析 中,笔者提到了自动配置类配置SqlSessionFactory
的 MybatisPlusAutoConfiguration#sqlSessionFactory()
方法 ,可以看到本文中自定义的 SQL 注入器通过 @Bean
托管给 Spring 后,也是在这里存入到 MyBatis-plus
的 GlobalConfig
全局缓存中的
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// TODO 使用 MybatisSqlSessionFactoryBean 而不是 SqlSessionFactoryBean
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
......
// TODO 此处必为非 NULL
GlobalConfig globalConfig = this.properties.getGlobalConfig();
......
// TODO 注入sql注入器
this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
......
factory.setGlobalConfig(globalConfig);
return factory.getObject();
}
private <T> void getBeanThen(Class<T> clazz, Consumer<T> consumer) {
if (this.applicationContext.getBeanNamesForType(clazz, false, false).length > 0) {
consumer.accept(this.applicationContext.getBean(clazz));
}
}
2.2 自定义 SQL 注入器的使用
MyBatis-plus 源码解析 分析了 MyBatis-plus
配置工作的主要流程,SQL 注入器的使用也在其中,这一部分本文不再重复。简单来说,就是根据 MyBatis-plus
提供的操作数据库的通用方法给每一个 Mapper 准备对应的 MappedStatement
对象的过程
2.3 Mapper 操作数据库的实现
这部分和本文的相关性比较大,但是内容细节非常多,为了分析的完整,读者可参考 MyBatis Mapper 操作数据库源码流程总结 做大致了解,关于 SQL 语句的细节处理可参考 MyBatis-plus 转化处理 SQL 语句的源码分析