MyBatis多数据库支持databaseId为null问题解决
本文主要讲述MyBatis多数据库支持databaseId为null的问题如何解决,在此之前,我们先来看下Mybatis如何配置支持多数据库。
MyBatis多数据库支持
让一个项目支持不同的数据库在企业开发中是一个比较常见的需求,即一套代码兼容不同的数据库。由于不同的数据库支持的sql语法稍有差别,所以某些功能需要根据数据库的不同书写不同的sql语句。对于这种需求,首先能够想到的解决方案就是针对不同的数据库维护不同的mapper.xml文件,但是这种方案会严重增加开发和维护的成本。因为不同数据库支持的语法大部分都是相同的,不同的毕竟是少数,我们希望只重写不同的部分而重用相同的部分。
针对这种情况,MyBatis提供了解决方案,即databaseIdProvider和databaseId。通过MyBatis提供的这种功能,我们就只需要维护一套mapper.xml文件即可。
MyBatis多数据库支持配置
第一步:databaseIdProvider配置
为支持多厂商特性,只要像下面这样在 mybatis-config.xml 文件中加入 databaseIdProvider 即可:
<databaseIdProvider type="DB_VENDOR">
<!-- name是数据库厂商名,value是你自己的数据库标识名 -->
<property name="SQL" value="sqlserver"/>
<property name="MS-SQL" value="sqlserver"/>
<property name="Microsoft SQL Server" value="sqlserver"/>
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
<property name="Adaptive Server Enterprise" value="sybase"/>
<property name="MySQL" value="mysql" />
</databaseIdProvider>
在每一个property标签中,name代表数据库的productName(DatabaseMetaData#getDatabaseProductName()),value是用户自定义的databaseId名称。
第二步:databaseId配置
在映射文件中根据不同databaseId数据库,执行不同的SQL:
示例一:
<select id="selectMenuTreeByUserId" resultMap="SysMenuResult" databaseId="mysql">
SELECT
m.menu_id,
m.parent_id,
m.menu_name,
m.path,
m.component,
m.visible,
m.STATUS
FROM
sys_menu m
ORDER BY
m.parent_id,
m.order_num
</select>
示例二:
<select id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult">
SELECT
m.menu_id,
m.parent_id,
m.menu_name,
m.path,
m.component,
m.visible,
m.STATUS,
<if test="_databaseId == 'mysql'">
ifnull(m.perms,'')
</if>
<if test="_databaseId == 'sqlserver'">
isnull(m.perms,'')
</if>
as perms,
m.create_time
FROM
sys_menu m
ORDER BY
m.parent_id,
m.order_num
</select>
如上,配置好databaseIdProvider和databaseId映射之后,即可测试mybatis连接不同数据库功能。
databaseId为null问题分析及解决
用户出现的问题:用户登录系统时报错:示例二对应的SQL出现as perms不完整,存在语法错误的问题。
出现问题场景:偶发,非必现
初步判断可能是_databaseId为null导致SQL解析问题;
接下来我们看下什么情况下会存在databaseId为null,首先,我们来看下databaseIdProvider默认实现类VendorDatabaseIDProvider:
public class VendorDatabaseIdProvider implements DatabaseIdProvider {
private Properties properties;
@Override
public String getDatabaseId(DataSource dataSource) {
if (dataSource == null) {
throw new NullPointerException("dataSource cannot be null");
}
try {
return getDatabaseName(dataSource);
} catch (Exception e) {
LogHolder.log.error("Could not get a databaseId from dataSource", e);
}
return null;
}
@Override
public void setProperties(Properties p) {
this.properties = p;
}
private String getDatabaseName(DataSource dataSource) throws SQLException {
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
for (Map.Entry<Object, Object> property : properties.entrySet()) {
if (productName.contains((String) property.getKey())) {
return (String) property.getValue();
}
}
// no match, return null
return null;
}
return productName;
}
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
try (Connection con = dataSource.getConnection()) {
DatabaseMetaData metaData = con.getMetaData();
return metaData.getDatabaseProductName();
}
}
private static class LogHolder {
private static final Log log = LogFactory.getLog(VendorDatabaseIdProvider.class);
}
}
从源码中可以看到,在dataSource.getConnection()异常,或无匹配的property映射配置时,databaseId均可能出现为null的情况。
由于我们使用的是mysql和sqlserver数据库,所以不存在无匹配property映射配置的问题,所以初步判断是dataSource.getConnection()异常的情况。
问题进一步分析:由于我们的应用直接装在客户的windows机器上,mysql服务和smaple应用服务均配置为Windows服务,且都是开机自动启动。无先后顺序及依赖关系,所以可能存在smaple服务先启动,mysql服务后启动的情况,这种情况下,dataSource.getConnection()有可能存在异常的情况。
用户问题复现
关闭mysql服务:
net stop mysql
关闭smaple服务:
net stop smaple
然后先启动smaple服务:
net start smaple
间隔10s后再启动mysql服务:
net start mysql
然后再访问应用,此时可复现示例二SQL不完成语法错误的问题。
问题总结
所以对于这种多个服务之间存在依赖关系的情况, 我们必须保证先启动mysql服务,然后再启动smaple应用服务。另外由于我们一套代码虽然支持多种数据库,但同一服务只会连接某一种数据库,不会在不同类型数据库之前动态切换,故针对databaseId为空的问题,我们也可以通过实现DatabaseIdProvider接口,自定义databaseId获取逻辑。
解决方法
方法一:设置服务之间的依赖关系(推荐)
以管理员身份打开cmd窗口,设置服务之间的依赖关系:
C:\Windows\system32>sc config smaple depend=mysql
[SC] ChangeServiceConfig 成功
即设置smaple服务依赖mysql服务,启动smaple服务前会先启动mysql服务。
方法二:实现DatabaseIdProvider接口,自定义databaseId的获取逻辑
在这里,我们也可以在VendorDatabaseIdProvider的基础上,通过给databaseId赋默认值,间接解决databaseId为null的问题,自定义实现类CustomDatabaseIdProvider代码如下:
public class CustomDatabaseIdProvider implements DatabaseIdProvider {
/**
* 设置databaseId默认值
*/
private static String DEFAULT_DATABASE_ID = "mysql";
private Properties properties;
@Override
public void setProperties(Properties p) {
this.properties = p;
}
@Override
public String getDatabaseId(DataSource dataSource) throws SQLException {
if (dataSource == null) {
throw new NullPointerException("dataSource cannot be null");
}
try {
return getDatabaseName(dataSource);
} catch (Exception e) {
CustomDatabaseIdProvider.LogHolder.log.error("Could not get a databaseId from dataSource", e);
}
return DEFAULT_DATABASE_ID;
}
private String getDatabaseName(DataSource dataSource) throws SQLException {
String productName = getDatabaseProductName(dataSource);
if (this.properties != null) {
for (Map.Entry<Object, Object> property : properties.entrySet()) {
if (productName.contains((String) property.getKey())) {
return (String) property.getValue();
}
}
// no match, return default databaseId
return DEFAULT_DATABASE_ID;
}
return productName;
}
private String getDatabaseProductName(DataSource dataSource) throws SQLException {
try (Connection con = dataSource.getConnection()) {
DatabaseMetaData metaData = con.getMetaData();
return metaData.getDatabaseProductName();
}
}
private static class LogHolder {
private static final Log log = LogFactory.getLog(CustomDatabaseIdProvider.class);
}
}
注意:此处CustomDatabaseIdProvider只解决了databaseId为空的问题,但在同一服务在不同数据库之间切换时,就不能够通过默认值来解决。还需根据不同的DataSource,指定不同的databaseId,所以不同场景需做不同的扩展处理。
(完)