借助Mybatis提供的 databaseId特性,我们可以实现让应用同时支持多种类型的数据库。
0. 目录这里写目录标题
1. 测试用例
相关的配置和测试用例如下 (这里我们以源生的mybatis为例,与SpringBoot的集成留待读者自行解读):
-
mybatis-config.xml
配置文件
<environments default="development">
<environment id="development">
....
</environment>
</environments>
<!-- 启用databaseId支持 -->
<databaseIdProvider type="DB_VENDOR">
<property name="Oracle" value="oracle"/>
</databaseIdProvider>
<mappers>
......
</mappers>
- Mapper类
// Mapper
public interface MultiDbTestMapper {
int getMtHisTableExists(@Param("dbName") String dbName, @Param("tableName") String tableName);
}
- Mapper文件
// mapper xml
<mapper namespace="xxx.mapper.MultiDbTestMapper">
<!-- 查看某表是否存在 -->
<select id="getMtHisTableExists" resultType="java.lang.Integer">
select count(*) ICOUNT from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=#{dbName} and TABLE_NAME=upper(#{tableName})
</select>
<!-- oracle方言查看某表是否存在 -->
<select id="getMtHisTableExists" resultType="java.lang.Integer" databaseId="oracle">
SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME=upper(#{tableName})
</select>
<!-- sqlserver方言查看某表是否存在 -->
<select id="getMtHisTableExists" resultType="java.lang.Integer" databaseId="sqlserver">
select count(*) from dbo.sysobjects where id = object_id(upper(#{tableName})) and OBJECTPROPERTY(id, N'IsUserTable') = 1
</select>
</mapper>
- 测试用例。
@Autowired
private MultiDbTestMapper mapper;
@Test
public void testName() throws Exception {
Console.log(mapper.getMtHisTableExists("xxx_boot", "xxx_code"));
}
- 用例执行结果:
- mybatis将根据实际配置的数据库地址,从相同id的映射语句(
<select>/<insert>/<update>/<delete>
)中抓取相对应的进行执行。 - 以上操作由mybatis自行完成,使用者只需要正确配置数据库链接即可。无需其他额外操作。
- mybatis将根据实际配置的数据库地址,从相同id的映射语句(
2. 原理解析
先说结论:
- mybatis在启动阶段,根据数据源的配置,将xml映射文件中对应数据库类型的 xml节点语句加载到内存中,以供之后的调用。
- 以上述"getMtHisTableExists"为例,只会有其中一个被加载到内存中。
源码解读:
-
与当前环境下所使用的DataBase相适应的databaseId的获取。
//================= XMLConfigBuilder.databaseIdProviderElement() , 对应于上述测试用例中mybatis-config.xml的配置 // 1. 以下方法将为 Configuration 中的 databaseId 字段赋值。 // 2. 赋值逻辑大致是通过用户注册的自定义DatabaseIdProvider实现类来完成 // 3. DatabaseIdProvider默认实现为 VendorDatabaseIdProvider . 其中databaseId的获取逻辑为:根据实际的DataSource类型来获取配置的对应databaseId private void databaseIdProviderElement(XNode context) throws Exception { DatabaseIdProvider databaseIdProvider = null; if (context != null) { String type = context.getStringAttribute("type"); // awful patch to keep backward compatibility if ("VENDOR".equals(type)) { // 在 Configuration 的构造函数中注册 type = "DB_VENDOR"; } Properties properties = context.getChildrenAsProperties(); databaseIdProvider = (DatabaseIdProvider) resolveClass(type).getDeclaredConstructor().newInstance(); databaseIdProvider.setProperties(properties); } Environment environment = configuration.getEnvironment(); if (environment != null && databaseIdProvider != null) { // 从实际使用的数据源中获取当前适用的databaseId String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); configuration.setDatabaseId(databaseId); } }
-
databaseId生效。
//================= XMLStatementBuilder.parseStatementNode() public void parseStatementNode() { String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); // 如果当前 <select>/<update>/<insert>/<delete>节点上的 databaseId 属性值 与 全局配置上的 databaseId 不一致, 则直接跳过对其的解析, 不会将其加载到内存中 if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } String nodeName = context.getNode().getNodeName(); ...... } private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { if (requiredDatabaseId != null) { return requiredDatabaseId.equals(databaseId); } if (databaseId != null) { return false; } id = builderAssistant.applyCurrentNamespace(id, false); if (!this.configuration.hasStatement(id, false)) { return true; } // skip this statement if there is a previous one with a not null databaseId MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2 return previous.getDatabaseId() == null; }
3. databaseId的其它应用
-
databaseId的另外一种使用方式:
@Select
注解。// 注解的方式实现SQL, 效果等同于上述测试用例中的映射文件 // 底层支撑类: MapperAnnotationBuilder.AnnotationWrapper @Select(value = "select count(*) ICOUNT from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA=#{dbName} and TABLE_NAME=upper(#{tableName})", databaseId = "mysql") @Select(value = "SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME=upper(#{tableName})", databaseId = "oracle") int getMtHisTableExistsByAnnotation(@Param("dbName") String dbName, @Param("tableName") String tableName);
-
通过占位符,在映射语句中获取当前的databaseId。(支撑类为
DynamicContext
)<select id="getMtHisTableExists" resultType="java.lang.Integer" databaseId="oracle"> SELECT '${_databaseId}' as databaseId FROM USER_TABLES WHERE TABLE_NAME=upper(#{tableName}) </select>