ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DatabaseId 实战:让 MyBatis 一套代码适配 MySQL 与 Oracle

DatabaseId 实战:让 MyBatis 一套代码适配 MySQL 与 Oracle

数据库切换到底怎么玩?同一个项目要同时兼容 MySQL 和 Oracle,Mapper 里的 SQL 写着写着就变成了“两份代码两个世界”。这个问题我踩过不少坑,最后靠 MyBatis 原生提供的 DatabaseId 机制把两套 SQL 收了回来。今天这篇就围绕 Spring Boot + MyBatis Plus 项目里 DatabaseId 的实际使用,把配置、写法、坑和排查思路完整捋一遍,适合正在做多数据库兼容、或者准备接手这种项目的同学参考。

先说结论:DatabaseId 不是 MyBatis Plus 的私有功能,它是 MyBatis 自带的“数据库方言识别”机制。MyBatis Plus 因为底层还是 MyBatis 的会话工厂,所以天然支持,只是自动装配层面需要我们多补一个 Bean。它的核心价值在于:同一个 Mapper XML 里可以写多套相同 ID 的 SQL 片段,每套用databaseId标记属于哪个数据库;应用启动时,MyBatis 通过DatabaseIdProvider检测当前连接的是哪类数据库,只加载匹配的那一套 SQL。这样代码仓库里只有一套 Mapper 接口,部署到不同数据库环境的客户现场时,不需要改代码、不需要替换 XML,启动即适配。

1. DatabaseId 到底在解决什么问题

1.1 数据库方言差异与多环境部署的痛点

做过传统企业项目交付的人应该都有这种经历:开发阶段全员 MySQL,功能验证没问题,到了客户现场,生产环境是 Oracle 或者 SQL Server,于是一大堆 SQL 开始冒红。最常见的就是分页、日期格式化、字符串拼接、空值处理这些点。比如 MySQL 分页写LIMIT #{start}, #{limit},换成 Oracle 就得套三层子查询或者用OFFSET ... FETCH;日期格式化一个用DATE_FORMAT,一个用TO_CHAR。如果项目里只有两三条这种 SQL,硬着头皮改还能忍,但几十上百条的时候就完全不是一回事了。

更麻烦的是,这种差异往往不是一个客户环境独有。这个项目要部署到 A 客户,那个项目要部署到 B 客户,两边数据库不同,你就得维护两套 XML 或者两套分支代码。改了一个逻辑,另一套可能漏改;功能上线前要在两套数据库环境分别回归。这种“通过判断代码路径来切 SQL”的方案,我早期做过,维护成本高到让人怀疑人生。

后面我用过根据环境变量动态加载不同 mapper-locations 的路子,也算能跑,但配置繁琐,而且 MyBatis 解析时机、多模块打包、测试环境切换都不够清爽。真正解决这个问题,还得回到 MyBatis 自己提供的 DatabaseIdProvider 机制。

1.2 DatabaseIdProvider 的工作链路

MyBatis 在构建Configuration的时候,会调用配置好的DatabaseIdProvider,从当前数据源连接里读取DatabaseMetaData.getDatabaseProductName(),拿到数据库产品名,比如 MySQL 返回MySQL,Oracle 返回Oracle,PostgreSQL 返回PostgreSQL。这个产品名经过VendorDatabaseIdProvider的Properties映射,变成我们自定义的短字符串,比如mysql、oracle,最终存在Configuration.databaseId字段上。

紧接着,MyBatis 解析 Mapper XML 中的每个语句节点时,会检查databaseId属性。这里有三个关键动作:

  • 如果语句节点显式写了databaseId="oracle",那么只有当当前配置的 databaseId 也是oracle时才会加载这个语句;
  • 如果语句节点没写databaseId属性,MyBatis 会把当前配置的 databaseId 当作这个语句的 databaseId 来绑定;
  • 只要配置了 provider,所有不匹配当前数据库的语句都会被跳过。

这个“加载时过滤”的设计非常关键,它意味着 MySQL 环境和 Oracle 环境虽然读的是同一个 XML 文件,但最终实际注册进配置的 MappedStatement 只有当前数据库匹配的那一套。你不需要在代码里写if (isOracle) doSomething(),数据库类型判断在 SqlSessionFactory 构建阶段就完成了。

我个人的理解是:DatabaseId 解决的是“同一套代码部署到不同数据库环境”的静态适配问题。它不解决运行中频繁切换库的问题,这一点后面讲坑的时候还会重点提。

2. 基础工程搭建:Spring Boot + MyBatis Plus 环境准备

2.1 依赖与版本选型

我用得比较稳的组合是 Spring Boot 2.7.18 + MyBatis Plus 3.5.3.2。如果项目已经升级到 Spring Boot 3,需要注意 MyBatis Plus 要换用mybatis-plus-spring-boot3-starter,因为 Spring Boot 3 用的是jakarta.*命名空间,老包会直接报类找不到。

pom.xml 里核心依赖就这几样:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里放 H2 依赖不是因为正事要连它,而是给“本地没有 Oracle 环境但想验证 DatabaseId 链路”的同学一条活路。后面实战部分我会专门演示怎么用 H2 模拟不同数据库分支。

2.2 基础配置与 Mapper 扫描

application.yml 先保持最简配置:

spring: datasource: url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true

启动类上记得加@MapperScan,让 Mapper 接口能被扫描注册。这里有个容易被新手忽略的点:MyBatis Plus 的 mapper-locations 默认路径是classpath*:/mapper/**/*.xml,如果你把 XML 放在别的目录,记得在配置里显式改掉,否则运行时会报 “Invalid bound statement (not found)”。

实体类用一个简单的SysUser演示,字段就是id、name、status、createTime:

@Data @TableName("sys_user") public class SysUser { private Long id; private String name; private Integer status; private LocalDateTime createTime; }

工程能起来、能连上库跑通一个基础查询之后,我们再往里面加 DatabaseId 相关的东西。

3. 核心配置:让 MyBatis Plus 识别当前数据库类型

3.1 注册 DatabaseIdProvider Bean

这一步是整个链路里最容易被忽略的。很多同学在 XML 里写好了databaseId="mysql",结果启动直接报 SQL 重复定义,原因就是容器里压根没有DatabaseIdProvider,MyBatis 把它当成“未启用多数据库支持”,把 XML 里的所有语句都原样加载了一遍,带 databaseId 和不带 databaseId 的同名语句自然就撞上了。

正确做法是手动注册一个 provider。MyBatis 官方提供了现成的实现VendorDatabaseIdProvider,我们只需要把它声明成 Spring Bean 并配置好数据库产品名和自定义 ID 的映射:

@Configuration public class DatabaseIdConfig { @Bean public DatabaseIdProvider databaseIdProvider() { VendorDatabaseIdProvider provider = new VendorDatabaseIdProvider(); Properties properties = new Properties(); properties.setProperty("MySQL", "mysql"); properties.setProperty("Oracle", "oracle"); properties.setProperty("PostgreSQL", "postgresql"); properties.setProperty("H2", "h2"); provider.setProperties(properties); return provider; } }

这里特别提醒一个细节:VendorDatabaseIdProvider的Properties的 key 是 JDBC 驱动返回的原始数据库产品名,大小写是敏感的。MySQL 驱动返回的是MySQL而不是mysql,Oracle 驱动返回的是Oracle而不是oracle。如果你只写了properties.setProperty("mysql", "mysql"),检测结果会直接落空,Configuration.getDatabaseId()拿到的可能是个不匹配的值。

如果不确定驱动返回什么产品名,可以在启动时临时打印一下:

@Component public class DatabaseIdRunner implements ApplicationRunner { @Resource private DataSource dataSource; @Override public void run(ApplicationArguments args) throws Exception { try (Connection connection = dataSource.getConnection()) { System.out.println("DatabaseProductName: " + connection.getMetaData().getDatabaseProductName()); } } }

我第一次写这个映射的时候就把大小写写错了,结果 provider 返回的是 null,_databaseId怎么判断都不走分支,排查了半天才发现是 properties 的 key 没对上。

3.2 MyBatis Plus 会把 provider 自动塞给 SqlSessionFactory

注册完 Bean 之后,MyBatis Plus 的自动配置会检查容器里有没有DatabaseIdProvider,有的话就设置到MybatisSqlSessionFactoryBean上。这块不需要我们额外写代码,Spring Boot 的自动装配帮我们处理掉了。

但有个前提:MybatisPlusAutoConfiguration扫描到DatabaseIdProviderBean 的方式是@Autowired(required = false),如果你的项目里有多个 SqlSessionFactory(比如多数据源场景),每个 factory 都需要单独设置 provider,自动配置只会照顾到默认的哪一个。这个等我讲到常见坑的时候再展开。

验证 provider 是否生效,最直接的一招是在ApplicationRunner里取一次SqlSessionFactory:

@Component public class DatabaseIdCheckRunner implements ApplicationRunner { @Resource private SqlSessionFactory sqlSessionFactory; @Override public void run(ApplicationArguments args) { System.out.println("[databaseId] current = " + sqlSessionFactory.getConfiguration().getDatabaseId()); } }

如果启动日志打印出[databaseId] current = mysql,说明整条链路已经通了。后面所有 XML 里的_databaseId判断才有意义。

4. 在 Mapper XML 里按数据库写 SQL 的两种实战姿势

4.1 姿势 A:一个 statementId 配多套 databaseId 语句

这是 DatabaseId 最直白的用法。同一个select节点,ID 保持一样,databaseId属性不同,MyBatis 启动时只会加载当前数据库匹配的那一个。以用户列表查询为例:

<select id="selectUserListByStatus" resultType="com.example.demo.entity.SysUser"> SELECT id, name, status, create_time FROM sys_user WHERE status = #{status} <if test="_databaseId == 'oracle'"> ORDER BY id OFFSET #{start} ROWS FETCH NEXT #{limit} ROWS ONLY </if> <if test="_databaseId == 'mysql'"> ORDER BY id LIMIT #{start}, #{limit} </if> </select>

这里我用的是_databaseId条件分支,但如果要演示显式databaseId属性写法,可以改成两套同 ID 的完整节点:

<select id="selectUserListByStatus" resultType="com.example.demo.entity.SysUser" databaseId="mysql"> SELECT id, name, status, create_time FROM sys_user WHERE status = #{status} ORDER BY id LIMIT #{start}, #{limit} </select> <select id="selectUserListByStatus" resultType="com.example.demo.entity.SysUser" databaseId="oracle"> SELECT id, name, status, create_time FROM sys_user WHERE status = #{status} ORDER BY id OFFSET #{start} ROWS FETCH NEXT #{limit} ROWS ONLY </select>

这种写法适合整套 SQL 差异很大、几乎等于重写的情况。例如某个复杂统计查询,Oracle 里要用ROW_NUMBER() OVER (PARTITION BY ...),MySQL 里要变着法子用变量或者窗口函数替代,两套 SQL 结构完全不同。硬塞进一个<choose>里会让 XML 变得又长又乱,不如直接按照databaseId拆成两个节点,职责清晰。

需要注意:使用姿势 A 时,要么所有环境都用显式databaseId标记,要么只在 XML 里保留一套默认语句,不要混着写“一套不带 databaseId 的默认 SQL + 一套带 databaseId 的特化 SQL”。因为配置了 provider 之后,不带 databaseId 的语句也会被绑定上当前数据库 ID,此时如果再出现一个显式声明同 ID 的语句,MyBatis 会直接报 “Mapped Statements collection already contains value”,具体现象和原因我放在最后的排查表里详细说。

4.2 姿势 B:一个语句内用 _databaseId 做分支

当两套 SQL 差异只集中在少量片段的时候,我更推荐用_databaseId参数配合<if>或<choose>在同一个语句里处理。_databaseId是 MyBatis 动态 SQL 内置的一个参数,它的值就是Configuration.getDatabaseId(),也就是我们前面通过 provider 设置好的当前数据库标识。

举个例子,用户列表查询同时要对时间字段做格式化、分页、按状态过滤。不同数据库的日期函数不同,分页语法不同,但整体 SQL 骨架一样,这种情况下把它拆成两个 statement 反而阅读起来费劲。用一个 statement +_databaseId分支,差异点就在眼皮子底下:

<select id="selectUserListByStatus" resultType="java.util.Map"> SELECT id, name, <choose> <when test="_databaseId == 'oracle'"> TO_CHAR(create_time, 'YYYY-MM-DD HH24:MI:SS') AS create_time </when> <otherwise> DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') AS create_time </otherwise> </choose> FROM sys_user WHERE status = #{status} <choose> <when test="_databaseId == 'oracle'"> ORDER BY id OFFSET #{start} ROWS FETCH NEXT #{limit} ROWS ONLY </when> <otherwise> ORDER BY id LIMIT #{start}, #{limit} </otherwise> </choose> </select>

运行时,MyBatis 会把_databaseId解析成当前的数据库标识,选择对应的 SQL 片段组装出来。这种写法最大的好处是“差异在同一个语句里集中暴露”,后续维护时只要看这一个节点就能知道自己维护的库走了哪条逻辑。

不过它也有缺点:如果差异点很多,<choose>和<if>嵌套多了,XML 的可读性会快速下降。我自己大概是这么取舍的:差异片段不超过 SQL 总量的三分之一,用姿势 B;改到跟重写差不多,用姿势 A。

4.3 两种姿势的对比与选择建议

为了让大家决策得更快,我把两种方式的适用场景整理成了表格:

对比维度姿势 A(多套 databaseId 语句)姿势 B(_databaseId 分支)
SQL 差异程度差异大,基本两套结构差异小,局部片段不同
XML 可读性每套完整 SQL,结构清晰分支多了会显得臃肿
维护成本两套都要维护,容易漏改差异点在一个语句里,一目了然
启动加载只加载匹配当前库的语句加载一个语句,运行时分段选择
适合场景复杂报表、特殊数据库专属写法分页、函数差异、条件拼接

实际做项目,我一般两种混着用:整体查询骨架一致的用姿势 B,某几个特别复杂的报表查询用姿势 A。DatabaseId 机制没有限制你在同一个 XML 里只能选一种,怎么顺手怎么来。

5. 实战:一套用户列表查询接口适配 MySQL 与 Oracle

5.1 业务需求与 SQL 差异分析

假设我们有个用户管理页面,要按状态分页查询用户列表,并且列表里展示的create_time需要格式化到秒。这个需求在 MySQL 和 Oracle 上有两个明显差异:

  • 分页语法:MySQL 用LIMIT #{start}, #{limit};Oracle 12c 以上可以用OFFSET #{start} ROWS FETCH NEXT #{limit} ROWS ONLY,比老套的ROWNUM两层子查询干净得多。
  • 日期格式化:MySQL 的DATE_FORMAT占位符是%Y-%m-%d %H:%i:%s;Oracle 的TO_CHAR格式串是'YYYY-MM-DD HH24:MI:SS'。

这个场景用姿势 B 最合适,因为整个 SQL 的骨架都是SELECT ... FROM sys_user WHERE status = ? ORDER BY id ...,只有中间一小段函数和最后的分页段需要切换。

5.2 Mapper 接口与 XML 完整实现

Mapper 接口没什么特殊的,多个参数记得用@Param显式命名:

public interface SysUserMapper extends BaseMapper<SysUser> { List<Map<String, Object>> selectUserListByStatus( @Param("status") Integer status, @Param("start") Integer start, @Param("limit") Integer limit); }

XML 放在resources/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.example.demo.mapper.SysUserMapper"> <select id="selectUserListByStatus" resultType="java.util.Map"> SELECT id, name, <choose> <when test="_databaseId == 'oracle'"> TO_CHAR(create_time, 'YYYY-MM-DD HH24:MI:SS') AS create_time </when> <otherwise> DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') AS create_time </otherwise> </choose> FROM sys_user WHERE status = #{status} <choose> <when test="_databaseId == 'oracle'"> ORDER BY id OFFSET #{start} ROWS FETCH NEXT #{limit} ROWS ONLY </when> <otherwise> ORDER BY id LIMIT #{start}, #{limit} </otherwise> </choose> </select> </mapper>

特别注意:XML 里如果 SQL 片段中出现<、>、&这些特殊字符,必须使用转义形式,比如&lt;代表小于号,或者用<![CDATA[ ... ]]>包裹。上面的例子刻意避开了写成ROWNUM &lt;= #{limit}这种老写法,直接用OFFSET ... FETCH,省去转义的麻烦,也更贴近现代 Oracle 的最佳实践。

接口调用时,传status = 1, start = 0, limit = 10,MySQL 环境会执行:

SELECT id, name, DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') AS create_time FROM sys_user WHERE status = 1 ORDER BY id LIMIT 0, 10

Oracle 环境会执行:

SELECT id, name, TO_CHAR(create_time, 'YYYY-MM-DD HH24:MI:SS') AS create_time FROM sys_user WHERE status = 1 ORDER BY id OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY

两者的返回字段结构一致,前端完全无感。

5.3 本地没有 Oracle,怎么验证分支逻辑

很多项目本地根本没装 Oracle,验证多数据库适配总不能每次跑到客户现场。我的做法是用 H2 数据库的兼容模式去模拟,成本极低,效果足够验证链路。

H2 的DatabaseMetaData.getDatabaseProductName()固定返回H2,所以我们需要在 provider 的映射里加一行properties.setProperty("H2", "oracle"),让 H2 连接在检测时被当成 Oracle 来对待。这样我们可以用同一套代码、同一套 XML,切换数据源 URL 就能分别走 MySQL 分支和 Oracle 分支。

MySQL 环境连接串:

spring: datasource: url: jdbc:mysql://localhost:3306/demo driver-class-name: com.mysql.cj.jdbc.Driver

模拟 Oracle 环境时,把数据源切到 H2 的 Oracle 兼容模式:

spring: datasource: url: jdbc:h2:mem:demo;MODE=Oracle;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1 driver-class-name: org.h2.Driver username: sa password:

H2 的 Oracle 模式支持TO_CHAR和OFFSET ... FETCH这些语法,足以验证 SQL 分支是否真的切换成功。如果你手头有可用的 MySQL 环境,建议真实跑一遍 MySQL 分支;Oracle 分支用 H2 模拟,这样的组合能覆盖大多数开发场景。

6. 高频问题与排查技巧速查

6.1 启动报 “Mapped Statements collection already contains value”

这个问题我在前面已经预告过。最常见的原因就是:XML 里同时存在“不带 databaseId 的默认语句”和“带 databaseId 的语句”,而两者 ID 完全相同。配置了DatabaseIdProvider之后,不带 databaseId 的语句也会被绑定当前数据库的 ID,于是 MyBatis 发现两个同 ID 同 databaseId 的 MappedStatement,自然就冲突了。

顺着这个思路就能排查:检查所有同 ID 的 select/update/insert/delete 节点,要么全部去掉 databaseId 属性,要么每个环境都显式把 databaseId 写全,不要让“默认 + 特化”混在一起。我在项目里用姿势 B(_databaseId分支)之后,这类问题几乎绝迹。

6.2 _databaseId 一直是 null,或者分支永远走不对

先确认容器里到底有没有注册DatabaseIdProviderBean。没有 provider 时,Configuration.getDatabaseId()为 null,动态 SQL 里_databaseId == 'oracle'永远为 false,所有分支都会走<otherwise>或直接漏掉。

再检查VendorDatabaseIdProvider的 Properties 映射 key 是否和驱动返回的DatabaseProductName大小写一致。MySQL 驱动返回MySQL,不是你随手写的mysql;SQL Server 驱动返回Microsoft SQL Server,中间还有空格。拿不准就直接打印DatabaseProductName看一眼。

还有一种情况是数据源没被正确检测。如果项目用了动态数据源,provider 在构建 SqlSessionFactory 时连接的是主数据源,而实际业务跑在另一个库上,_databaseId检测的结果和真实执行库可能对不上,需要确认检测数据源与实际业务数据源的关系。

6.3 部署到多数据源环境,DatabaseId 为什么不动态切换

这是很多人最容易产生误解的地方。Configuration.getDatabaseId()是在 SqlSessionFactory 初始化阶段,通过 provider 检测数据源后一次性写入的。也就是说,DatabaseId 是“部署时确定”的静态值,不是“运行中随数据源切换”的动态值。

如果项目里用动态数据源方案,比如一个 MySQL 库、一个 Oracle 库,运行过程中通过@DS注解切换数据源,此时底层只有一个 SqlSessionFactory,它的 databaseId 仍然是初始化时检测到的那个值,_databaseId不会跟随切换而变化。这种情况不要指望 DatabaseId 来区分,正确做法是:

  • 每个数据源单独构建 SqlSessionFactory,并且为每个 factory 设置对应的DatabaseIdProvider;
  • 或者在 Mapper 层把多数据库差异抽象成独立接口,按数据源类型加载不同的 Mapper 实现。

我早期在这上面栽过跟头,以为动态数据源 + DatabaseId 是万能组合,结果切换后 SQL 一直走错分支,排查了很久才意识到 databaseId 根本不感知运行时切换。

6.4 与 MyBatis Plus 分页插件的协同使用

MyBatis Plus 自带的分页插件PaginationInnerInterceptor是另一个维度的方言适配。它负责在 SQL 执行前拦截并改写分页语句,本身需要指定数据库方言类型,比如new PaginationInnerInterceptor(DbType.MYSQL)。

如果你已经用分页插件处理了分页,那么 DatabaseId 就不需要再操心LIMIT和OFFSET这些分页差异了,只处理函数、语法层面的差异,比如前面示例里的日期格式化。两层职责一拆,XML 能比上面写的还干净:

<select id="selectUserListByStatus" resultType="java.util.Map"> SELECT id, name, <choose> <when test="_databaseId == 'oracle'"> TO_CHAR(create_time, 'YYYY-MM-DD HH24:MI:SS') AS create_time </when> <otherwise> DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') AS create_time </otherwise> </choose> FROM sys_user WHERE status = #{status} ORDER BY id </select>

分页插件在拦截器层统一处理方言,DatabaseId 只管 SQL 片段差异,两者互不干扰,配合起来很顺。要注意的是,如果连接的是动态数据源,分页插件的DbType也需要按数据源区分,固定写死DbType.MYSQL而在 Oracle 数据源上执行同样会出问题,这一点和 DatabaseId 的“静态检测”问题同宗同源。

6.5 排查建议:把 databaseId 打印进启动日志

最后分享一个我自己的小习惯:项目启动时,一定会把当前 databaseId 打印出来。写法很简单,一个ApplicationRunner就够:

@Component public class DatabaseIdStartupLogger implements ApplicationRunner { @Resource private SqlSessionFactory sqlSessionFactory; @Override public void run(ApplicationArguments args) { System.out.println("[databaseId] current databaseId = " + sqlSessionFactory.getConfiguration().getDatabaseId()); } }

这样每次部署环境变了、数据源改了,第一眼就能确认 MyBatis 到底识别成了什么数据库,而不是等到接口调用才发现 SQL 分支不对。这个习惯帮我节省了大量“看起来正常但实则走了错误分支”的排查时间。

项目里如果真的同时面对 MySQL、Oracle、PostgreSQL 这类多数据库交付场景,DatabaseId 是我目前用下来最顺手的方案。SQL 差异小就塞进_databaseId分支,差异大就拆成多套databaseId语句,部署到哪里检测到哪里,代码仓库里始终只有一套 Mapper。暂时没条件装真实 Oracle 数据库的,H2 兼容模式加上映射配置也能把链路跑通,本地验证足够了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进