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

    • 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执行器类型
    • 分页插件

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 &lt;= #{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&amp;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 &lt;= #{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 代码中指定不同的 environment id

分析

  • 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 可以动态切换运行时更换数据库连接,发现仍执行旧 SQLdatabaseId 在 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 的正确配置与常见陷阱。

上一页
dataSource
下一页
mappers