乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

_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.7MySQLmysql
Oracle 11g/12cOracleoracle
Microsoft SQL ServerMicrosoft SQL Serversqlserver
PostgreSQLPostgreSQLpostgresql
DB2DB2db2
H2H2h2

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&amp;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 &lt;= #{offset} + #{limit}
                ORDER BY id
            ) WHERE rn &gt; #{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>

操作前数据

idnameagemajorscore
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 环境)

idnameagemajorscore
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>

操作前数据

idnameagemajorscore
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(大翔):

idinfo
1姓名:大翔 | 专业:计算机科学

查询 id = 4(黄俪):

idinfo
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 &lt;= #{offset} + #{limit} ORDER BY id
            ) WHERE rn &gt; #{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 的开发和运维变得透明可控。

上一页
script 元素:在注解映射器中启用动态 SQL
下一页
动态 SQL 中插入脚本语言