databaseIdProvider
导学
本节学习目标:
- 理解
databaseIdProvider解决的核心痛点:同一套应用需要适配多种数据库方言 - 掌握
DB_VENDOR内置实现的使用方式 - 能够在 Mapper XML 中通过
databaseId属性编写数据库特定的 SQL - 了解多数据库适配在实际项目中的落地策略
定义
企业级应用常面临多数据库共存的场景:
- 开发环境使用 MySQL 5.7,成本低、易部署
- 生产核心系统使用 Oracle,追求稳定性与商业支持
- 数据分析模块使用 PostgreSQL,利用其高级分析函数
不同数据库的 SQL 方言存在差异:分页语法(MySQL 的 LIMIT vs Oracle 的 ROWNUM)、字符串拼接函数(MySQL 的 CONCAT vs Oracle 的 ||)、日期函数、主键生成策略等各不相同。
如果为每种数据库维护一套独立的 Mapper XML,维护成本极高且容易遗漏同步。databaseIdProvider 的核心使命就是让 MyBatis 自动识别当前连接的数据库厂商,并在 Mapper 中根据 databaseId 匹配执行对应数据库的 SQL 片段,实现"一份 Mapper,多库适配"。
适用位置与核心属性
databaseIdProvider 位于 mybatis-config.xml 中 environments 之后、mappers 之前。
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="PostgreSQL" value="postgresql"/>
</databaseIdProvider>
| 属性 | 是否必填 | 说明 |
|---|---|---|
type | 必填 | 数据库标识提供者类型。内置支持 DB_VENDOR,也可自定义实现 DatabaseIdProvider 接口 |
DB_VENDOR 的 property 子标签
| 属性名 | 说明 |
|---|---|
name | 数据库产品名称(通过 DatabaseMetaData.getDatabaseProductName() 获取) |
value | 映射后的 databaseId 标识符,用于 Mapper XML 中匹配 |
DB_VENDOR的实现逻辑:获取 JDBC 连接的DatabaseMetaData,读取getDatabaseProductName()返回的字符串,与配置的name进行前缀匹配(不区分大小写),匹配成功则返回对应的value作为databaseId。
核心原理
多数据库适配流程图
Mapper XML 中的使用方式:
<select id="selectByPage" resultType="employee">
<!-- 无 databaseId 的语句作为兜底 -->
SELECT * FROM employee
</select>
<select id="selectByPage" resultType="employee" databaseId="mysql">
SELECT * FROM employee LIMIT #{offset}, #{limit}
</select>
<select id="selectByPage" resultType="employee" databaseId="oracle">
SELECT * FROM (SELECT ROWNUM rn, t.* FROM employee t WHERE ROWNUM <= #{end}) WHERE rn > #{start}
</select>
规则:当存在多条同
id的语句时,MyBatis 优先选择databaseId与当前环境匹配的语句;若无匹配,则选择无databaseId的兜底语句。
完整示例
场景说明
乐途公司员工管理系统需要同时支持 MySQL 5.7 和 Oracle 11g 两种数据库。员工分页查询在两种数据库中的语法不同,需要通过 databaseIdProvider 实现自动适配。
操作前的状态
未配置 databaseIdProvider 时,Mapper 中只能写一种分页语法,切换数据库时必须修改 XML 或维护多套文件。
完整配置代码
步骤一:配置 databaseIdProvider
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.feixiang.entity"/>
</typeAliases>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/employee_db?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="mysql123"/>
</dataSource>
</environment>
<environment id="oracle">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="oracle.jdbc.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@192.168.1.200:1521:ORCL"/>
<property name="username" value="feixiang"/>
<property name="password" value="oracle123"/>
</dataSource>
</environment>
</environments>
<!-- 多数据库适配配置 -->
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
</databaseIdProvider>
<mappers>
<mapper resource="mapper/EmployeeMapper.xml"/>
</mappers>
</configuration>
步骤二:编写多数据库兼容的 Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feixiang.mapper.EmployeeMapper">
<!-- 兜底语句:无 databaseId,适用于所有数据库 -->
<select id="selectById" resultType="employee">
SELECT employee_id, employee_name, department_code, create_time
FROM employee
WHERE employee_id = #{id}
</select>
<!-- MySQL 分页语法 -->
<select id="selectByPage" resultType="employee" databaseId="mysql">
SELECT employee_id, employee_name, department_code, create_time
FROM employee
ORDER BY employee_id
LIMIT #{offset}, #{pageSize}
</select>
<!-- Oracle 分页语法 -->
<select id="selectByPage" resultType="employee" databaseId="oracle">
SELECT * FROM (
SELECT ROWNUM rn, t.employee_id, t.employee_name, t.department_code, t.create_time
FROM employee t
WHERE ROWNUM <= #{endRow}
ORDER BY employee_id
) WHERE rn > #{startRow}
</select>
<!-- MySQL 插入后获取自增主键 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="employeeId" databaseId="mysql">
INSERT INTO employee (employee_name, department_code)
VALUES (#{employeeName}, #{departmentCode})
</insert>
<!-- Oracle 插入前通过序列获取主键 -->
<insert id="insert" databaseId="oracle">
<selectKey keyProperty="employeeId" resultType="int" order="BEFORE">
SELECT employee_seq.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO employee (employee_id, employee_name, department_code)
VALUES (#{employeeId}, #{employeeName}, #{departmentCode})
</insert>
</mapper>
步骤三:Java 测试代码
public class DatabaseIdProviderDemo {
public static void main(String[] args) throws Exception {
// 测试 MySQL 环境
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory mysqlFactory = new SqlSessionFactoryBuilder().build(is, "mysql");
try (SqlSession session = mysqlFactory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
List<Employee> list = mapper.selectByPage(0, 10);
System.out.println("MySQL 环境分页查询结果数: " + list.size());
}
// 测试 Oracle 环境
is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory oracleFactory = new SqlSessionFactoryBuilder().build(is, "oracle");
try (SqlSession session = oracleFactory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
List<Employee> list = mapper.selectByPage(0, 10); // 内部自动使用 Oracle 分页 SQL
System.out.println("Oracle 环境分页查询结果数: " + list.size());
}
}
}
实际效果/结果
MySQL 环境控制台输出:
MySQL 环境分页查询结果数: 10
Oracle 环境控制台输出:
Oracle 环境分页查询结果数: 10
效果验证:
- 同一
Mapper接口的selectByPage方法,在 MySQL 环境下实际执行LIMIT分页 SQL - 在 Oracle 环境下实际执行
ROWNUM嵌套分页 SQL - 插入操作在 MySQL 下使用
useGeneratedKeys,在 Oracle 下使用序列selectKey - 切换环境时无需修改 Mapper XML,只需在 Java 代码中指定不同的
environmentid
分析
DB_VENDOR通过DatabaseMetaData.getDatabaseProductName()识别数据库,MySQL 返回"MySQL",Oracle 返回"Oracle"。property的name支持前缀匹配,因此"MySQL"可以匹配"MySQL"、"MySQL Community Server"等变体- 同
id的多条语句中,有databaseId的语句优先级高于无databaseId的兜底语句 - 若当前环境的
databaseId为null(未匹配任何数据库),则只使用无databaseId的语句 - 对于差异极大的 SQL(如 Oracle 的
START WITH ... CONNECT BY层级查询),databaseId机制比动态 SQL 的<choose>更清晰,因为后者将多库逻辑混杂在一个语句中
易错场景/常见误区
| 误区 | 错误表现 | 正解 |
|---|---|---|
property 的 name 写错大小写 | databaseId 为 null,始终使用兜底语句 | DB_VENDOR 的匹配不区分大小写,但建议与 DatabaseMetaData 返回值保持一致 |
同 id 的多条语句都没有 databaseId | 启动报 Mapped Statements collection already contains value for xxx | 同 namespace + id 必须唯一,除非通过 databaseId 区分 |
有 databaseId 的语句覆盖了兜底语句后,切换未配置的数据库 | 该语句找不到,执行报错 | 确保每种目标数据库都有对应的 databaseId 语句,或保留无 databaseId 的兜底语句 |
认为 databaseId 可以动态切换 | 运行时更换数据库连接,发现仍执行旧 SQL | databaseId 在 SqlSessionFactory 构建时确定,运行时不可变。切换数据库需重建 SqlSessionFactory |
在 databaseIdProvider 中配置了数据库但未在 Mapper 中使用 | 配置无效,感觉没有起到适配作用 | databaseIdProvider 只是"识别"数据库,真正的适配需要在 Mapper 语句中显式声明 databaseId 属性 |
使用 DB_VENDOR 但连接被连接池包装 | DatabaseMetaData 返回的是连接池代理类信息 | DB_VENDOR 内部会解包 Unwrap 获取真实连接,通常无问题。若使用特殊连接池,可自定义 DatabaseIdProvider |
面试考点
Q1:databaseIdProvider 的作用是什么?DB_VENDOR 是如何识别数据库的?
A:
databaseIdProvider用于在 MyBatis 中实现多数据库适配。DB_VENDOR是其内置实现,它通过获取 JDBC 连接的DatabaseMetaData,调用getDatabaseProductName()获取数据库产品名称,然后与 XML 中配置的property进行前缀匹配,返回映射后的databaseId。Mapper 中的语句可以通过databaseId属性与当前环境匹配。
Q2:如果 Mapper 中同时存在有 databaseId 和无 databaseId 的同 id 语句,MyBatis 如何选择?
A:MyBatis 优先选择
databaseId与当前环境匹配的语句。如果找不到匹配的databaseId,则退而选择无databaseId的兜底语句。这种机制允许开发者为特定数据库编写优化 SQL,同时为其他数据库提供通用实现。
Q3:databaseId 在运行时可以动态改变吗?
A:不可以。
databaseId在SqlSessionFactory构建时通过databaseIdProvider解析并写入Configuration对象,之后不可变。如果应用需要切换数据库类型,必须重新创建SqlSessionFactory实例。
小结
databaseIdProvider 是 MyBatis 应对多数据库部署的优雅方案。通过 DB_VENDOR 自动识别数据库厂商,配合 Mapper 中 databaseId 属性的精确匹配,可以在一份 XML 中容纳多种 SQL 方言。记住:配置识别只是第一步,真正的适配需要在每个差异点上编写带 databaseId 的语句,并保留无 databaseId 的兜底实现。
下一章引子
全局配置的最后一环,是告诉 MyBatis 去哪里找到 Mapper XML 文件和 Mapper 接口。mappers 元素提供了四种引入方式,从单个文件到整包扫描,选择不当会导致启动失败或映射器遗漏。接下来,我们将学习 mappers 的正确配置与常见陷阱。