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,所以不同场景需做不同的扩展处理。

(完)

最后修改于:2022年10月26日 13:19

添加新评论