1. 先说结论DatabaseId 不是 Mybatis Plus 专属功能却最该被它用好很多同学第一次看到DatabaseId这个配置项容易误以为是 Mybatis Plus 后来推出的新特性。实际上它最早是 MyBatis 原生框架里就有的能力Mybatis Plus 在封装过程中把它完整保留了下来并且在多数据源、多数据库兼容的场景下这个配置项的价值被放大了很多倍。我最初接触 DatabaseId 是因为一个很现实的需求一套业务代码要同时跑在 MySQL 和某国产数据库上不可能为每种数据库各维护一套 Mapper XML更不想在 Java 代码里写一堆if (dbType ...)的土判断。当时翻遍了官方文档发现 DatabaseId 这个机制正好能解决同一份 SQL 映射根据不同数据库方言自动切换实现的问题。简单解释一下 DatabaseId 是什么它是 MyBatis 在解析 Mapper XML 时识别到的一个数据库标识你可以把它理解成给当前数据库连接发的一个身份证。MyBatis 会根据这个身份证去加载对应数据库专用的 SQL 片段。比如selectUser这个查询你可以为 MySQL 写一套分页写法为 Oracle 写另一套然后通过 DatabaseId 自动选择业务代码零侵入。需要说明的是本篇内容基于常见实践和个人实测经验补充了具体步骤Mybatis Plus 版本差异可能导致个别 API 名称不同但核心机制是稳定的。2. 理解 DatabaseId 的工作原理比背配置更重要2.1 数据库方言问题的本质做过老项目维护的同学都有体会数据库方言差异是跨数据库迁移时最头疼的问题之一。分页语法不同、序列自增方式不同、日期函数不同、字符串拼接符号不同这些细节在写 SQL 的时候不注意换库就翻车。比如最简单的分页MySQL 用LIMIT offset, pageSizeOracle 12c 以下用ROWNUM嵌套子查询PostgreSQL 用LIMIT ... OFFSET ...SQL Server 用OFFSET ... FETCH NEXT ... ROWS ONLY如果项目里只有一套 SQL换数据库就必须改代码。而 DatabaseId 给了你一条新思路SQL 映射文件里的select、insert等标签可以针对不同数据库提供不同版本由框架在运行时自动决定加载哪一份。2.2 DatabaseId 的解析链从连接到 XML来看 MyBatis 拿到 DatabaseId 的完整链路应用启动时MyBatis 会拿到配置好的databaseIdProvider实现类。该实现类通过DatabaseMetaData获取当前数据库的产品名例如MySQL、Oracle、PostgreSQL。框架将这个产品名映射为你自定义的标识例如mysql、oracle、pg。解析 Mapper XML 时框架逐个检查select等标签上的databaseId属性与当前数据库的标识匹配。匹配到的 SQL 片段会被注册进对应的 MappedStatement运行时就执行它。这个机制的本质是按标识隔离 SQL 变体而不是对 SQL 文本做动态字符串替换所以性能和安全性都有保障。2.3 核心 APIDatabaseIdProvider 接口自定义 DatabaseId 支持的关键在DatabaseIdProvider接口MyBatis 自带了一个实现类VendorDatabaseIdProvider。它的作用是读取DatabaseMetaData.getDatabaseProductName()返回的原始数据库名再通过别名映射表转换成自定义的短标识。举个例子某国产数据库的getDatabaseProductName()返回的可能是很长的产品名你需要把它映射成dm或kingbase这样的短标识。如果一直用原始长名字写 Mapper 的时候既麻烦又容易拼错。3. 从零实操在 Spring Boot Mybatis Plus 项目中启用 DatabaseId3.1 准备一个可复现的最小工程用 Spring Initializr 创建项目时至少引入以下依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.x/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意我的实测环境是 Spring Boot 2.7.x Mybatis Plus 3.5.3JDK 8。如果你用的是 Spring Boot 3.x务必确认 Mybatis Plus 版本兼容否则启动阶段就可能报ClassNotFoundException。3.2 核心配置步骤注册 DatabaseIdProvider第一步是在启动类或者某个配置类里显式声明DatabaseIdProvider的 Bean。如果不注册MyBatis 不会启用这个机制所有的 SQL 都只走无databaseId的默认版本。Configuration public class MybatisPlusConfig { Bean public DatabaseIdProvider databaseIdProvider() { VendorDatabaseIdProvider provider new VendorDatabaseIdProvider(); Properties properties new Properties(); properties.setProperty(MySQL, mysql); properties.setProperty(Oracle, oracle); properties.setProperty(PostgreSQL, pg); properties.setProperty(DM DBMS, dm); // 达梦 properties.setProperty(KingbaseES, kingbase); // 人大金仓 provider.setProperties(properties); return provider; } }这里最关键的是一点Properties的 key 必须使用DatabaseMetaData.getDatabaseProductName()返回的原始名称value 则是你自定义的短标识。不同驱动返回的原始名称可能不一样比如 MySQL 驱动可能返回MySQL也可能带版本号。稳妥的做法是先打个测试接口打印出实际返回的产品名再决定映射关系。3.3 验证 DatabaseId 是否真的生效很多同学配置完就以为万事大吉实际上必须验证。最简单的方式是在启动日志里搜索DatabaseId相关输出或者写一个临时接口Autowired private SqlSessionFactory sqlSessionFactory; GetMapping(/dbid) public String getDatabaseId() { return sqlSessionFactory.getConfiguration().getDatabaseId(); }访问这个接口后控制台或浏览器返回mysql说明 DatabaseIdProvider 注册成功并且被 MyBatis 正确解析了。这一步跑不通的话后续所有基于 DatabaseId 的 SQL 切换都不会生效。3.4 在 Mapper XML 中使用 databaseId 属性配置完成后接下来就是在 Mapper XML 中写多版本 SQL。注意databaseId是select、insert、update、delete以及sql标签上的可选属性不是独立子标签。下面是一个典型的分页查询示例select idselectUserPage resultTypecom.example.entity.User SELECT * FROM t_user ORDER BY create_time DESC /select select idselectUserPage databaseIdmysql resultTypecom.example.entity.User SELECT * FROM t_user ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /select select idselectUserPage databaseIdoracle resultTypecom.example.entity.User SELECT * FROM ( SELECT t.*, ROWNUM rn FROM ( SELECT * FROM t_user ORDER BY create_time DESC ) t WHERE ROWNUM lt; #{offset} #{pageSize} ) WHERE rn gt; #{offset} /select这里有个非常重要的规则如果存在不带databaseId的默认 SQL同时也存在带databaseId的专用 SQL那么 MyBatis 在解析时会如何处理根据 MyBatis 源码逻辑MapperBuilderAssistant会为同一个 MappedStatement 注册多个版本的 SQL并在运行时根据当前Configuration.getDatabaseId()选择匹配的那个。如果当前数据库有匹配的专用版本就用专用版如果没有匹配的就用默认版本。所以我的建议是始终保留一份不带databaseId的默认 SQL 作为兜底。这样可以防止某个数据库的专用 SQL 漏写时直接报 No SQL found 的尴尬。3.5 在注解 SQL 中使用 databaseIdMapper 注解方式同样支持databaseId。比如Select(SELECT * FROM t_user) ListUser listAll(); Select(SELECT * FROM t_user LIMIT #{limit}) Options(databaseId mysql) ListUser listAllWithLimit(Param(limit) int limit);不过从我实际项目经验看多版本 SQL 一旦超过两套还是尽量收敛到 XML 文件里维护更清晰。注解方式写少量变体可以写多了可读性会很差。4. 踩坑实录从配置没生效到SQL 选错版本的排查链路4.1 坑一DatabaseIdProvider 没被 MyBatis 识别有次我在一个老项目里加了上述配置启动后没有任何报错但访问测试接口返回的databaseId是null。排查步骤第一步确认配置类是否真的被 Spring 扫描到了。如果配置类放到了启动类所在包之外并且没有ComponentScan显式指定这个 Bean 根本不会被加载。第二步确认是否同时存在多个SqlSessionFactory的BeanPostProcessor干扰。如果项目里手动创建了SqlSessionFactoryBean并且没有把databaseIdProvider传入Spring Boot 自动配置的MybatisPlusAutoConfiguration注入的 provider 不会生效。第三步确认VendorDatabaseIdProvider是否在类路径上。它位于org.apache.ibatis.datasource包中通常不会缺但如果你用的是精简版依赖可能被裁剪掉。我的最终结论如果项目里自定义了SqlSessionFactory一定要在构建工厂时显式设置databaseIdProvider否则自动配置的 provider 会被跳过。4.2 坑二XML 里同时出现多个 databaseId 版本时默认 SQL 被覆盖这个坑非常隐蔽。有一次我写了三套 SQL其中两套带了databaseId一套没带。我预期的是有匹配就用匹配版没匹配用默认版但实际发现默认版在某些环境下不生效。查看 MyBatis 源码后发现在XMLMapperBuilder解析过程中如果同一个id下面有多个databaseId不同的 SQL最终 MappedStatement 里只会保留一个 SqlSource而不是多个。具体的取舍逻辑是如果当前 databaseId 恰好匹配某条 SQL则使用这条。如果当前 databaseId 没有匹配项则使用不带 databaseId 的那条。如果既没有匹配项也没有不带 databaseId 的默认 SQL则抛出异常。理论上我的预期是对的但问题出在我把默认 SQL 放在带databaseId的 SQL 之后并且 XML 标签顺序不对。MyBatis 解析时是按文档顺序处理的后解析的条目可能覆盖先解析的。最后我通过调整 XML 顺序解决了问题把默认 SQL 放最前面专用版本按字母顺序排列减少不确定性。4.3 坑三不同数据库驱动返回的 DatabaseProductName 五花八门这个坑是最折磨人的。比如同一个数据库老版本驱动可能返回MySQL新版本驱动可能返回MySQL 8.0.33。而VendorDatabaseIdProvider在匹配时并不是拿完整的getDatabaseProductName()去比对而是先尝试精确匹配再尝试前缀匹配。具体源码逻辑是VendorDatabaseIdProvider内部会遍历配置的 Properties用databaseProductName.startsWith(key)来判断。所以理论上MySQL能前缀匹配MySQL 8.0.33。但某些国产数据库的 JDBC 驱动实现不规范返回的产品名里包含特殊字符或者前后空格这时候前缀匹配就可能失败。解决办法很简单在测试环境写一个 endpoint打印实际返回的产品名然后按实际值去配置映射。不要凭经验臆测。GetMapping(/dbmeta) public String getDbMeta() throws SQLException { try (Connection conn dataSource.getConnection()) { DatabaseMetaData meta conn.getMetaData(); return meta.getDatabaseProductName() | meta.getDatabaseProductVersion(); } }4.4 坑四Mybatis Plus 的分页插件与 DatabaseId 冲突Mybatis Plus 官方分页插件PaginationInnerInterceptor默认会根据数据库类型自动选择分页方言这本是个好消息。但如果你同时又在 XML 里手动写了LIMIT或者ROWNUM两者就会叠加导致 SQL 异常。具体表现查询时 Mybatis Plus 先对 SQL 做了一次方言分页处理再加上你自己手写的LIMIT最终生成的 SQL 可能变成LIMIT 10 LIMIT 5这种非法语句。我的建议是如果用了 Mybatis Plus 分页插件XML 里就别再手动写分页语法让插件统一处理。DatabaseId 专注解决的是非分页类的方言差异比如日期函数、自增主键回填方式、字符串拼接、布尔值表示等。5. DatabaseId 的典型应用场景拆解5.1 场景一多数据库兼容部署这是最常见的用法。一套代码需要同时支持主流的 MySQL、Oracle、PostgreSQL或者向国产数据库迁移时快速适配。把方言差异 SQL 通过 databaseId 隔离业务层零改动。5.2 场景二同一功能在不同库采用不同实现策略比如批量插入MySQL 可以通过INSERT INTO ... VALUES (...), (...), (...)一条语句插入多行。Oracle 则需要INSERT ALL INTO t ... INTO t ... SELECT * FROM dual或者使用BEGIN ... END;匿名块。这两种写法差异巨大用 DatabaseId 可以各写各的互不干扰。5.3 场景三读写分离或多数据源环境如果你用的是多数据源组件如dynamic-datasource不同数据源连接到不同数据库类型DatabaseId 仍然是根据当前连接自动识别的。只要每个数据源对应的数据库产品名不同SQL 切换就会自动发生。有个需要注意的点动态数据源切换时MyBatis 的DatabaseId是在 SqlSessionFactory 初始化时确定的不是每次请求都去探测。如果多个数据源分属不同数据库类型你可能要配置多个SqlSessionFactory每个传入对应的databaseIdProvider或直接指定固定的 databaseId。6. 多数据源场景下 DatabaseId 的进阶写法6.1 全局配置一个 provider还是每个数据源单独配置这里不能一概而论。如果你的多个数据源都属于同一类数据库比如都是 MySQL全局一个 provider 就够了。如果差异很大建议为每个数据源单独定义SqlSessionFactory并显式指定 databaseId。Mybatis Plus 官方多数据源方案里可以这样为每个数据源设置 databaseIdBean Primary public SqlSessionFactory mysqlSqlSessionFactory(Qualifier(mysqlDataSource) DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factory new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setGlobalConfig(globalConfig); // 显式指定当前工厂的 databaseId factory.setDatabaseIdProvider(new VendorDatabaseIdProvider() {{ Properties props new Properties(); props.setProperty(MySQL, mysql); setProperties(props); }}); return factory.getObject(); }注意如果每个工厂都设置了 provider那么每个Configuration的 databaseId 会在初始化时被解析好。运行时如果数据源切换了但 SqlSessionFactory 没变databaseId 也不会变这是常被误解的点。6.2 多数据源 Mybatis Plus 插件的组合经验实测下来多数据源和多数据库类型组合时最容易出的问题不是 DatabaseId 本身而是多个SqlSessionFactory之间MybatisPlusInterceptor的注册重复。拦截器是全局的如果一个拦截器被注册到多个工厂会导致 SQL 被多次改造。我的建议是每个SqlSessionFactory单独new出一个MybatisPlusInterceptor不要共享同一个实例。至于 DatabaseId只需要确保每个工厂都能正确识别自己的数据库类型即可。7. DatabaseId 与 Mybatis Plus 条件的联动LambdaQueryWrapper 是否受影响7.1 结论先行LambdaQueryWrapper 是由 Mybatis Plus 自己生成的 SQL它在生成过程中并不知道 databaseId 的存在。也就是说不管当前跑在 MySQL 还是 Oracle 上只要是 Mybatis Plus 的内置方法如selectById、selectList生成的 SQL 都是基于 Mybatis Plus 自己的方言处理器。这听起来似乎意味着 DatabaseId 对 Mybatis Plus 内置方法没用但实际不是。Mybatis Plus 的DbType和DatabaseIdProvider是两套独立的机制DbType用于 Mybatis Plus 生成 SQL 时选择合适的方言例如分页插件。DatabaseIdProvider用于 MyBatis 解析 Mapper XML 时选择自定义 SQL 版本。两者互不干扰但也各有边界。如果你的需求是只靠 Mybatis Plus 内置方法支持跨库你需要配置的是DbType不是 DatabaseId。如果你的需求是自定义 SQL 需要跨库切换那 DatabaseId 就是正解。7.2 实际项目中的混合用法我目前维护的一个项目就是这么做的简单 CRUD 用 Mybatis Plus 的BaseMapper内置方法配置好DbType后自动适配分页。复杂多表关联查询、报表统计 SQL 写在 XML 里用databaseId区分不同数据库的方言差异。两者通过TableName、TableField注解统一映射实体互不冲突。这种混合思路可以兼顾开发效率和灵活度是目前我在跨库项目里最推崇的写法。8. 实测比较关于 databaseId 的常见误区澄清8.1 databaseId 不是动态 SQL 标签有同学以为databaseId可以像if标签一样在运行时根据参数动态判断这是误解。databaseId是静态的、在 Mapper 解析期间固定的属性它只根据数据库类型选择不会因为查询参数的变化而变化。如果需要在运行时根据参数动态选择 SQL应该用choose、when标签配合自定义参数判断而不是 databaseId。8.2 databaseId 不是配置在 application.yml 里的一个开关许多教程会让你在 yml 里写database-id: mysql这个配置项在 MyBatis 里确实存在但它的优先级和 DatabaseIdProvider 的关系很多人搞不清楚。如果你在mybatis-plus.configuration.database-id里配死了某个值它会在所有环境里强制使用这个 databaseIdDatabaseIdProvider 自动探测的结果会被覆盖。这在开发环境可能没问题但部署到不同数据库的生产环境就会出错。我的建议是不要硬编码 databaseId 到全局配置里除非你的系统只对接一种数据库。否则请使用 DatabaseIdProvider 自动探测。8.3 databaseId 的匹配是精确还是前缀匹配前面提到过VendorDatabaseIdProvider 在匹配时会先尝试完全匹配 key再尝试前缀匹配。但实际应用中发现如果数据库产品名里有空格或大小写不一致前缀匹配也可能失败。稳妥做法是把常见变体都配进 Propertiesproperties.setProperty(MySQL, mysql); properties.setProperty(MySQL 8.0, mysql); properties.setProperty(MySQL 8.0.33, mysql);虽然看起来冗余但在国产驱动和特殊版本驱动的场景下确实能救命。9. 实操总结一套可以直接照抄的启用步骤把前面的经验浓缩成下面这个清单照着做基本不会跑偏。9.1 环境确认确认 Spring Boot 版本与 Mybatis Plus 版本兼容。确认项目中没有自定义的 SqlSessionFactory 干扰自动配置。确认目标数据库的 JDBC 驱动版本一致。9.2 配置清单启动类或配置类中注册DatabaseIdProviderBean。在 Properties 中配置数据库产品名到短标识的映射。在 Mapper XML 中为不同数据库写专用 SQL并添加databaseId属性。始终保留一套不带 databaseId 的默认 SQL 兜底。9.3 验证清单调用临时接口输出databaseId确认非空。检查启动日志中 Mapper 解析是否注册了多个 SQL 变体。编写针对不同数据库的集成测试确认各版本 SQL 被正确选中。检查分页插件是否与手动分页 SQL 冲突。10. 最后分享几个实际项目中的小经验我在真实项目里踩过不少坑最想提醒的还是关于测试覆盖的问题。DatabaseId 机制本身很简单但它依赖数据库连接的真实元数据这意味着单元测试如果用了 H2 内存库H2 的产品名和线上 MySQL 完全不同加载的 SQL 也会不同。如果测试环境只用 H2线上却跑 MySQL很容易出现测试全绿上线炸了的情况。所以我的做法是在 CI 流水线里分别用 MySQL 和另一种目标数据库跑一遍集成测试至少要覆盖所有带 databaseId 的 SQL 片段。如果暂时没有条件搭多套数据库可以在本地准备两个 Docker 容器成本很低。另外如果项目里既有 Mybatis Plus 又有原生 MyBatis 的混合使用方式务必统一 SQL 的管理规范。我见过有的团队 XML 里一半 SQL 加了 databaseId一半没加时间一长根本没人敢动这些文件。最好在代码评审模板里固定一条规则凡是涉及数据库方言差异的 SQL必须显式声明 databaseId禁止裸写。最后一个小技巧如果数据库产品名变化频繁可以在配置类里引入一个日志打印启动时输出实际解析到的 productName方便快速定位匹配问题。用PostConstruct或者在DatabaseIdProvider的 Bean 初始化时打印即可。 提示以上内容是个人在 Spring Boot Mybatis Plus 项目中使用 DatabaseId 的完整实操记录包含原理、配置、踩坑和场景分析未经授权禁止转载。
阅读完成 · 觉得有帮助?