0%

tk.mybaits动态表名

tk.mybaits动态表名

背景

​ 由于目前项目中多采用模块化的方式进行组件的整合,在新服务的搭建时为了尽可能保证新服务指向的数据库中的表名统一于服务名,比如新服务是订单模块,希望这个模块指向的数据库中的表名都是以ORDER_开头。且一些二次开发的组件中也有使用mysql进行数据的相关处理,故希望可以通过统一处理的方式进行表名动态化。

跟踪

tk.mybatis的大致处理逻辑:

tk相关处理过程.png

  1. 扫描器会将所有被@Mapper标记的类通过SpringBean的创建对象流程中,然后创建相应的对象后,添加到SpringBean容器中。
  2. 将这个标记的类对象包装为MapperFactoryBean对象
  3. 待到创建bean流程的最后,也就是经过了初始化、后置处理器列表等扩展点的相关处理之后,通过包装对象MapperFactoryBean#afterPropertiesSet()进行tk.mybatis对象的二次处理。
  4. 通过这个后置方法,解析出这个@Mapper对象所对应的表实体的表名和其他一些在创建SQL所需要的配置信息。
1
2
3
4
5
6
7
public class EntityHelper {

/**
* 存储在这个map中 实体类 => 表对象
*/
private static final Map<Class<?>, EntityTable> entityTableMap = new ConcurrentHashMap<Class<?>, EntityTable>();
}
  1. 根据这个标记对象所继承的上层接口,逐个解析出对应的SQL语句。以下方代码为例,这里的上层接口CommonMapper继承于Mapper,而Mapper又继承于很多的上层接口,其中就有selectOne接口,那么在这一步就会根据第四步骤中解析出来的表信息和字段信息等,构建一个基于变量的SQL语句,如果业务层调用该方法时,仅将相关参数填充后就形成一条完整的SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Mapper
public interface XxxxMapper extends CommonMapper<XxxAttachment> {

}

public interface CommonMapper<T> extends Mapper<T> {
}

public interface Mapper<T> extends
BaseMapper<T>,
ExampleMapper<T>,
RowBoundsMapper<T>,
Marker {

}

public interface SelectOneMapper<T> {

/**
* 根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常,查询条件使用等号
*
* @param record
* @return
*/
@SelectProvider(type = BaseSelectProvider.class, method = "dynamicSQL")
T selectOne(T record);

}

处理方案

​ 根据以上的步骤,可以得出一个结论,如果想要动态(这个动态是指创建时动态,并不是指运行时动态)的修改表名,那么需要在第四步之后,第五步之前将entityTableMap中对应的表名修改为想要的表名。

​ 根据Springbean创建对象的逻辑,在初始化后置方法之前,会经过后置处理器列表,所以可以通过模拟一个后置处理器列表,提前对entityTableMap相关信息进行解析并缓存。

MapperFactoryBean 进行处理的主要逻辑:

  1. 根据XxxMapper类获取到对应的表实体

  2. 通过EntityHelper.initEntityNameMap()方法解析出这个表实体的相关信息,并缓存。

  3. XxxMapper.selectExample() 等等内置的通用接口解析为动态SQL语句,缓存在某个地方(这个我没有去找在哪里)

综上:只要能在 第3步之前将EntityHelper中的这个缓存中的EntityTable的表名称修改为相应的值,就能实现将表名动态化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class TablePrefixBeanPostProcessor implements BeanPostProcessor {

/**
* tk.mybatis 在 MapperFactoryBean 进行相关的逻辑处理,所以通过BeanPostProcessor,在执行MapperFactoryBean.afterPropertiesSet()方法之前,
* 进行一次预处理,将表相关信息通过EntityHelper.initEntityNameMap()方法提前添加到对应的entityTableMap<Class, EntityTable>缓存之后,
* 然后马上将这个实体所对应的EntityTable的表名进行需要的业务处理。
*
* @author xiaocainiaoya
* @date 2022/7/27 16:04:01
* @param bean
* @param beanName
* @return: java.lang.Object
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean.getClass() != null && bean.getClass().toString().contains("tk.mybatis.spring.mapper.MapperFactoryBean")){
Class<?> mapperClass = ((MapperFactoryBean)bean).getMapperInterface();
Type[] types = mapperClass.getGenericInterfaces();
ParameterizedType parameterizedType = (ParameterizedType) types[0];
Class<?> returnType = (Class<?>) parameterizedType.getActualTypeArguments()[0];

// 反射获取mapperHelper
MapperHelper mapperHelper = (MapperHelper) ReflectUtil.getFieldValue(bean, "mapperHelper");
// 提前解析这个bean所对应的表实体的相关信息
EntityHelper.initEntityNameMap(returnType, mapperHelper.getConfig());

// 是否需要修改表名
if(true){
EntityTable entityTable = EntityHelper.getEntityTable(returnType);
entityTable.setName("prefix" + entityTable.getName());
}
}

return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

return bean;
}


}

上方代码中的最后的判断语句可以根据相应的规则,比如说自定义一个注解,并且在注解中指定一个前缀的参数,标记在实体上,这里可以通过获取这个注解进行是否需要添加前缀的逻辑处理。

总结

​ 实际上以上这种场景应该在真是情况中少之又少,一般情况下并不会说想要统一某个服务中的所有表名前缀,但是在解决的问题的过程中,其实对springBean创建实体的流程,以及tk.mybatis对实体解析的相关逻辑都大致的过了一遍,加强了框架中一些细节处理的印象,对于后续如果出现一些bug可以尽快的定位到问题。

​ 实际上我并不是一下子就想到这么做,在想到这么做之前,我也经过了其他的尝试,比方说我最先想到的是通过写mybatis插件的方式,拦截出BoundSql具体的SQL语句,通过修改这个SQL语句达到目的,但是发现不同类型的SQL判断方式略有不同,操作难度大,所以一直往上层追溯,发现BoundSql中的语句是在Bean初始化过程就已经生成,后面才一点一点整理出上述步骤流程,从而找到下手的地方。

-------------本文结束感谢您的阅读-------------