_databaseId 与动态 SQL 的多数据库支持
导学
在企业级应用开发中,"一套代码适配多种数据库"是一个高频且棘手的诉求。开发团队可能在本地使用 MySQL 5.7 进行功能开发,而客户现场的生产环境却要求部署在 Oracle 或 SQL Server 上。不同数据库的 SQL 方言差异显著:分页语法不同、字符串拼接函数不同、自增主键机制不同……如果为每种数据库都维护一套独立的 Mapper,代码重复度极高,维护成本令人望而却步。
MyBatis 从全局配置到动态 SQL 提供了一套完整的多数据库适配方案。在 02 章中,我们已经学习了 databaseIdProvider——它在 mybatis-config.xml 中定义,负责在应用启动时识别当前连接的数据库类型,并赋予其一个厂商标识。而本章要介绍的 _databaseId,正是这个标识在动态 SQL 上下文中的具体化身。它是一个内置的 OGNL 变量,可以在 <if>、<choose> 等动态标签中被引用,从而让同一个 Mapper 根据运行时数据库类型,自动选择对应的 SQL 片段。
定义与作用
_databaseId 是 MyBatis 在执行动态 SQL 时,自动注入到 OGNL 上下文中的一个内置变量。它的值来源于 databaseIdProvider 对当前数据库连接的分析结果,通常对应数据库厂商的别名,如 mysql、oracle、sqlserver 等。
开发者可以在 Mapper XML 或 <script> 注解中,通过 OGNL 表达式(如 _databaseId == 'mysql')进行条件判断,从而在同一段映射逻辑中,为不同数据库编写差异化的 SQL 实现。这彻底解决了"一套 Mapper 走天下"的核心痛点。
核心原理
_databaseId 获取与使用流程
关键说明
| 阶段 | 组件 | 职责 |
|---|---|---|
| 启动期 | VendorDatabaseIdProvider | 读取 databaseIdProvider 配置,通过 JDBC 元数据识别数据库厂商 |
| 启动期 | Configuration | 存储解析后的 databaseId,供整个生命周期使用 |
| 运行期 | DynamicContext | 创建 OGNL 绑定时,自动将 databaseId 以 _databaseId 为名注入上下文 |
| 运行期 | OgnlCache | 解析并执行 _databaseId == 'mysql' 等条件表达式 |
值得注意的是,_databaseId 的变量名以下划线开头,这是 MyBatis 内置变量的命名惯例,目的是避免与用户传入的参数名冲突。
_databaseId 取值来源与常见标识值
| 来源 | 说明 |
|---|---|
databaseIdProvider 配置 | 在 mybatis-config.xml 中定义,通过 VendorDatabaseIdProvider 或自定义实现提供 |
| JDBC 元数据 | 底层调用 DatabaseMetaData.getDatabaseProductName() 获取原始厂商名称 |
| 别名映射 | VendorDatabaseIdProvider 通过 properties 中的前缀映射,将原始名称简化为短标识 |
常见数据库标识值对照表
| 数据库 | 原始产品名称(JDBC 返回) | 常用 databaseId |
|---|---|---|
| MySQL 5.7 | MySQL | mysql |
| Oracle 11g/12c | Oracle | oracle |
| Microsoft SQL Server | Microsoft SQL Server | sqlserver |
| PostgreSQL | PostgreSQL | postgresql |
| DB2 | DB2 | db2 |
| H2 | H2 | h2 |
mybatis-config.xml 中的典型配置
<?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>
<!-- 数据库厂商标识提供器 -->
<databaseIdProvider type="VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="Microsoft SQL Server" value="sqlserver"/>
<property name="PostgreSQL" value="postgresql"/>
</databaseIdProvider>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/flying_db?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/flying/mapper/StudentMapper.xml"/>
</mappers>
</configuration>
VendorDatabaseIdProvider 的工作原理是:将 JDBC 返回的数据库产品名称与 property 的 name 进行前缀匹配。例如,JDBC 返回 "MySQL",匹配到 <property name="MySQL" value="mysql"/>,最终 databaseId 就是 mysql。如果没有任何属性匹配,则 databaseId 为 null。
示例一:跨数据库分页查询
场景说明
学生管理系统需要实现分页查询功能。MySQL 5.7 使用 LIMIT 进行分页,Oracle 使用 ROWNUM 伪列,SQL Server 使用 OFFSET FETCH。同一个 Mapper 需要同时兼容这三种数据库。
mybatis-config.xml 配置
<databaseIdProvider type="VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="Microsoft SQL Server" value="sqlserver"/>
</databaseIdProvider>
Mapper XML
<?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.flying.mapper.StudentMapper">
<!-- 跨数据库分页查询学生 -->
<select id="findByPage" resultType="com.flying.entity.Student">
<!-- MySQL 分页语法 -->
<if test="_databaseId == 'mysql'">
SELECT id, name, age, major, score
FROM student
ORDER BY id
LIMIT #{offset}, #{limit}
</if>
<!-- Oracle 分页语法 -->
<if test="_databaseId == 'oracle'">
SELECT * FROM (
SELECT id, name, age, major, score, ROWNUM rn
FROM student
WHERE ROWNUM <= #{offset} + #{limit}
ORDER BY id
) WHERE rn > #{offset}
</if>
<!-- SQL Server 分页语法 -->
<if test="_databaseId == 'sqlserver'">
SELECT id, name, age, major, score
FROM student
ORDER BY id
OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
</if>
</select>
</mapper>
操作前数据
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
生成的 SQL 对比
MySQL 5.7 环境下(传入 offset = 0, limit = 3):
SELECT id, name, age, major, score
FROM student
ORDER BY id
LIMIT 0, 3
Oracle 环境下(传入 offset = 0, limit = 3):
SELECT * FROM (
SELECT id, name, age, major, score, ROWNUM rn
FROM student
WHERE ROWNUM <= 0 + 3
ORDER BY id
) WHERE rn > 0
SQL Server 环境下(传入 offset = 0, limit = 3):
SELECT id, name, age, major, score
FROM student
ORDER BY id
OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY
执行结果(MySQL 环境)
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
分析
这个示例展示了 _databaseId 最典型的应用场景。三种数据库的分页语法差异巨大,无法通过简单的参数化解决。通过 <if test="_databaseId == 'xxx'"> 将不同实现隔离在同一个 select 节点内,既避免了 Mapper 的重复定义,又保证了每种数据库都能获得最优的本地语法。
需要注意的是,Oracle 的 ROWNUM 是在结果集生成时分配的,因此必须将 ORDER BY 放在子查询内部,再在外层用 rn 过滤,否则分页顺序会错乱。这是 Oracle 分页的经典陷阱,与 MyBatis 无关,但需要在 SQL 编写时格外留意。
示例二:跨数据库字符串拼接
场景说明
查询时需要将学生的 name 和 major 拼接成一个字符串返回,格式为 "姓名:大翔 | 专业:计算机科学"。不同数据库的字符串拼接函数各不相同:MySQL 使用 CONCAT,SQL Server 使用 + 运算符,Oracle 使用 || 运算符。
mybatis-config.xml 配置
与示例一相同:
<databaseIdProvider type="VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="Microsoft SQL Server" value="sqlserver"/>
</databaseIdProvider>
Mapper XML
<?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.flying.mapper.StudentMapper">
<!-- 跨数据库字符串拼接查询 -->
<select id="findWithConcatInfo" resultType="java.util.HashMap">
SELECT id,
<!-- MySQL 使用 CONCAT 函数 -->
<if test="_databaseId == 'mysql'">
CONCAT('姓名:', name, ' | 专业:', major) AS info
</if>
<!-- SQL Server 使用 + 运算符 -->
<if test="_databaseId == 'sqlserver'">
'姓名:' + name + ' | 专业:' + major AS info
</if>
<!-- Oracle 使用 || 运算符 -->
<if test="_databaseId == 'oracle'">
'姓名:' || name || ' | 专业:' || major AS info
</if>
FROM student
WHERE id = #{id}
</select>
</mapper>
操作前数据
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
生成的 SQL 对比
MySQL 5.7 环境下(传入 id = 1):
SELECT id, CONCAT('姓名:', name, ' | 专业:', major) AS info
FROM student
WHERE id = ?
SQL Server 环境下(传入 id = 1):
SELECT id, '姓名:' + name + ' | 专业:' + major AS info
FROM student
WHERE id = ?
Oracle 环境下(传入 id = 1):
SELECT id, '姓名:' || name || ' | 专业:' || major AS info
FROM student
WHERE id = ?
执行结果(MySQL 环境)
查询 id = 1(大翔):
| id | info |
|---|---|
| 1 | 姓名:大翔 | 专业:计算机科学 |
查询 id = 4(黄俪):
| id | info |
|---|---|
| 4 | 姓名:黄俪 | 专业:信息安全 |
分析
字符串拼接是跨数据库兼容性问题的"重灾区"。CONCAT、+、|| 三种语法互不兼容,且没有统一的 JDBC 标准函数可以替代。_databaseId 让这种差异在 Mapper 层就被消化掉,上层业务代码完全无感知。
此外,这个示例也可以改用 <bind> 标签配合 _databaseId 实现更优雅的复用。例如,可以先用 <bind> 定义一个跨数据库的拼接变量,再在 SQL 中引用。不过,由于拼接语法差异过大,<bind> 更适合处理参数预处理,而非 SQL 函数替换。
与 bind 配合实现跨数据库兼容
<bind> 标签可以在 OGNL 表达式中创建变量,供后续 SQL 引用。结合 _databaseId,可以实现更精细的参数处理。
<select id="findByNameLike" resultType="com.flying.entity.Student">
<!-- 根据数据库类型决定通配符拼接方式 -->
<bind name="pattern" value="'%' + name + '%'"/>
SELECT id, name, age, major, score
FROM student
WHERE name LIKE #{pattern}
</select>
上面的 <bind> 是数据库无关的,但如果通配符本身在不同数据库中有差异(虽然 LIKE 的 % 在主流数据库中通用),可以进一步结合 _databaseId:
<select id="findByNameLike" resultType="com.flying.entity.Student">
<if test="_databaseId == 'mysql'">
<bind name="pattern" value="CONCAT('%', name, '%')"/>
</if>
<if test="_databaseId == 'oracle'">
<bind name="pattern" value="'%' || name || '%'"/>
</if>
SELECT id, name, age, major, score
FROM student
WHERE name LIKE #{pattern}
</select>
注意:
<bind>的value属性是 OGNL 表达式,不是 SQL 表达式。因此CONCAT在<bind>中无法直接调用(它是 SQL 函数而非 OGNL 函数)。上面的写法仅作概念演示,实际跨数据库通配符拼接更推荐在 Java 代码中预处理,或在 SQL 片段中直接用数据库函数。
易错场景
| 错误场景 | 错误表现 | 正确做法 |
|---|---|---|
databaseIdProvider 未配置或配置错误 | _databaseId 始终为 null,所有 <if test="_databaseId == ..."> 都不成立,可能返回空 SQL 或执行错误 | 确保 mybatis-config.xml 中正确配置了 databaseIdProvider,且 property 的 name 与 JDBC 返回的产品名前缀匹配 |
| 数据库标识值拼写不一致 | mybatis-config.xml 中配的是 mysql,Mapper 中写的是 mySQL,条件永远不成立 | 统一使用小写标识值,或在配置和 Mapper 中保持完全一致 |
多个 <if> 条件同时成立 | 生成的 SQL 包含多个数据库的片段,语法冲突 | 使用 <choose>、<when>、<otherwise> 替代多个 <if>,确保只进入一个分支 |
未处理 _databaseId 为 null 的情况 | 当数据库未被识别时,没有任何 SQL 片段被生成 | 添加 <otherwise> 分支作为兜底方案,或确保所有环境都能被正确识别 |
在注解 <script> 中使用 _databaseId 时未转义 | OGNL 表达式解析异常 | _databaseId 是合法变量名,无需转义,但要确保整个 <script> 标签结构正确 |
使用 choose 替代多个 if 的最佳实践
<select id="findByPage" resultType="com.flying.entity.Student">
<choose>
<when test="_databaseId == 'mysql'">
SELECT * FROM student ORDER BY id LIMIT #{offset}, #{limit}
</when>
<when test="_databaseId == 'oracle'">
SELECT * FROM (
SELECT id, name, age, major, score, ROWNUM rn
FROM student WHERE ROWNUM <= #{offset} + #{limit} ORDER BY id
) WHERE rn > #{offset}
</when>
<when test="_databaseId == 'sqlserver'">
SELECT * FROM student ORDER BY id OFFSET #{offset} ROWS FETCH NEXT #{limit} ROWS ONLY
</when>
<otherwise>
<!-- 兜底:如果数据库未被识别,抛出错误或提供默认实现 -->
SELECT * FROM student ORDER BY id
</otherwise>
</choose>
</select>
<choose> 确保只有一个分支会被选中,避免了多个 <if> 同时满足导致的 SQL 拼接灾难。
面试考点
Q1:_databaseId 和 databaseIdProvider 是什么关系?_databaseId 的值从哪里来?
A:
databaseIdProvider是全局配置层面的组件,在应用启动时通过 JDBC 元数据识别数据库厂商,生成一个字符串标识(如mysql),并存入Configuration。而_databaseId是运行时的 OGNL 上下文变量,MyBatis 在执行动态 SQL 前,自动将Configuration中的databaseId以_databaseId为名注入到DynamicContext中。两者是"配置定义"与"运行使用"的关系。
Q2:如果项目需要支持一种新的数据库(比如达梦数据库),需要做哪些改动?
A:三处改动:第一,在
mybatis-config.xml的databaseIdProvider中增加达梦的property映射(如<property name="DM" value="dm"/>);第二,在所有使用_databaseId做分支判断的 Mapper 中,增加达梦对应的<when>或<if>分支;第三,如果达梦的语法与已有数据库差异较大,需要为每个差异点补充达梦的 SQL 实现。
Q3:_databaseId 为 null 时会发生什么?如何避免因此导致的故障?
A:如果
databaseIdProvider没有匹配到任何数据库,或者当前环境没有配置databaseIdProvider,_databaseId会是null。此时所有test="_databaseId == 'xxx'"的条件都不成立,如果没有<otherwise>兜底,可能导致生成的 SQL 不完整甚至为空,最终抛出 SQL 异常。避免方法是:始终为<choose>提供<otherwise>分支,或者在启动时增加校验,确保databaseId不为null。
Q4:_databaseId 能否在注解的 <script> 中使用?与 XML 中使用有区别吗?
A:完全可以,且用法完全一致。在
@Select("<script>...")中,同样可以写<if test="_databaseId == 'mysql'">。_databaseId是 MyBatis 自动注入的上下文变量,与映射器的形式(XML 或注解)无关。唯一的区别是注解中需要额外注意<script>标签和特殊字符的转义。
小结
_databaseId 是 MyBatis 多数据库适配方案中的"最后一公里"。它承接了 databaseIdProvider 在启动期识别的数据库类型,将这一信息传递到运行期的动态 SQL 决策中,让同一个 Mapper 能够根据环境自动选择最优的 SQL 实现。
核心要点回顾:
_databaseId是 OGNL 内置变量,值来源于databaseIdProvider的数据库识别结果。- 通过
<if>或<choose>结合_databaseId,可以在同一 Mapper 中隔离不同数据库的方言差异。 - 分页、字符串拼接、函数调用是跨数据库适配的三大典型场景。
- 使用
<choose>替代多个<if>可以避免多分支同时生效的 SQL 灾难。 - 务必处理
_databaseId为null的兜底场景,增强系统健壮性。
下一章引子
到目前为止,我们已经掌握了 MyBatis 动态 SQL 的全部核心能力:条件判断、循环遍历、动态更新、注解中的 script 支持,以及跨数据库的 _databaseId 适配。这些技术足以应对日常开发中绝大多数的 SQL 动态化需求。
但动态 SQL 的灵活性也带来了新的挑战:当 <if> 嵌套三层、<foreach> 循环上千条记录时,如何确保生成的 SQL 是正确且高效的?如何在开发阶段就验证动态 SQL 的逻辑,而不是等到生产环境才暴露问题?下一章将介绍 MyBatis 的日志与调试技巧,学习如何通过日志输出、测试框架和性能分析工具,让动态 SQL 的开发和运维变得透明可控。