利益相关:笔者是多租户微信小程序 SaaS 项目的主力工程师,技术栈 Java 21 + Spring Boot,生产 PostgreSQL、日常开发与测试 H2,迁移分通用与 PG 专属双目录,85 个迁移文件推进到 V113,测试侧 250 个测试类、1241 个测试方法。两个库怎么分工、边界画在哪、踩过哪些坑,这篇一次讲完。代码与报错均为脱敏示意。
一、背景:为什么日常反馈不全用真 PG
我们三个人并行开发,测试每天要跑很多遍:写完一个方法跑一遍、提交前跑一遍、CI 上再跑一遍,1241 个测试方法就是按这个节奏滚出来的。这套节奏成立的前提是反馈快、环境轻。H2 是嵌入式库,跟着 JVM 进程起来、跟着进程销毁——不用先装一个数据库服务、不占固定端口、不常驻吃内存,每个测试类拿一个干净的库,跑完即焚,并行分支互不干扰。真 PG 是一个要单独启动、单独维护的服务,本地常驻一份,三个人的机器配置和耐心都吃不消。所以分法很自然:日常快速反馈用 H2,与真库行为强相关的验证留给 PG。省下的等待是实打实的,但"H2 通过"这四个字的含金量要先打个折——坑就埋在这里。
二、边界:H2 给不了的三样东西
- 行级安全(RLS)。 多租户系统的最后一道防线是 PG 的 RLS 策略:就算应用层查询漏了租户条件,数据库也会把别家的行挡在结果集外。H2 没有对应能力,这类策略只在 PG 上生效,也就只能在 PG 上验证。
- 锁与并发的部分行为。 行锁粒度、锁等待的触发时机、死锁检测与报错形态,两边实现不同。想复现"两个事务互相等对方"的场景,H2 上排不出来。
- 函数与类型细节。 jsonb 的运算符、序列的分配语义、超长文本的处理、字符串比较规则——单看每条都是小事,摞在一起,就是"本地绿、预发红"的全部来源。
迁移那一层我们已有现成分法:通用目录放两边行为一致的 DDL,PG 专属目录(RLS、jsonb、partial index)只在 PG 通道执行。这篇补的是测试层:把同样的思想搬到用例上——测试也按"依赖哪边的行为"分组。
三、踩坑实录:三个"本地绿、预发红"
坑一:超长字段,H2 放行,PG 拒收
现象:会员昵称列是 varchar(32),一条用例塞了 40 字的极端昵称验证展示截断,本地全绿;上预发,同一条数据当场报错:
ERROR: value too long for type character varying(32)
定位:H2 对超长 VARCHAR 的处理比 PG 宽松,写入没在约束处被拦下,测试于是"证明"了一个 PG 上根本不成立的行为。修法分两层:长度校验前移到应用层,非法数据到不了 DAO;"约束真的会拦人"这类断言整体挪进 PG 套件——约束语义,只有 PG 说了算。
坑二:JSON 字段,存进去和读出来的不是一个东西
现象:营销配置用了 JSON 字段,一条用例断言"写入后读出,内容相等"。本地稳定绿;预发偶尔红——不是每次都红,只有测试数据里恰好带重复键或多余空格时才红。
定位:PG 的 jsonb 存的是解析后的二进制表示——重复键只留最后一个、键序不保留、多余空格被归一化;H2 侧的存取更接近原文。同一份 JSON,两边读出来的字符串不一样,断言撞上的正是这层差异,测试数据的偶然性让它潜伏了很久。修法:断言从"字符串相等"改成"解析成对象后结构相等";涉及 jsonb 查询语义的用例整体划入 PG 套件,H2 侧只留序列化往返的测试。
坑三:批量插入,主键次序不是你以为的那样
现象:批量导入用例断言"插入 N 行后按主键升序读回,顺序与写入一致"。H2 全绿;预发红——读回顺序与写入顺序对不上。
定位:PG 的序列按 VALUES 行序取号,取出的号不因事务回滚退还,主键有空洞是正常态;H2 的自增分配策略不同,恰好让"连续且有序"长期成立。根子上是断言越界——把数据库的实现细节当成了业务承诺。修法:断言改成"主键唯一 + 业务键有序"(业务键是显式排序字段);凡是依赖数据库内部分配次序的断言,一律按坏味道处理。
四、测试矩阵:按行为分标签,不按快慢分
组织方式是 JUnit 5 的 @Tag:绝大多数用例打 fast,跑在 H2 上;凡是验证 PG 特有行为——RLS 拦截、jsonb 查询、约束语义、锁行为——的用例打 pg,跑在真 PG 上:
@Tag("pg") // 只在 PG 套件执行:跨租户读取必须被 RLS 拦下
class TenantIsolationPgTest {
// 预期:查询别家租户的行,结果集为空,而非依赖应用层过滤
}
CI 的排法:每次推送先跑 fast 套件,H2 内存库从零建 schema,覆盖绝大多数回归;PG 套件在合并主干前和夜间全量时跑,用 CI 环境里的真 PG 实例,从零建库、两个迁移目录全量执行——顺手兜住"迁移只在一种库上验证过"的风险。本地想跑 PG 套件,随时可以单独起服务,但它不进日常循环,不拖慢每天的节奏。review 也有一条对应规矩:新用例先看标签打没打对——该打 pg 的打了 fast,等于没测。
五、原则:双库是效率工具,不是保真工具
H2 的价值是快,不是像。它换来了高频反馈,代价是"绿"的含金量下降——H2 绿只说明逻辑大体对,不说明 PG 上成立。所以边界要画在行为差异上,不是画在跑得快慢上:断言依赖哪边的行为,就必须去哪边跑。保真没有捷径,只有一条路:让关键测试见到真 PG。RLS 拦不拦得住跨租户读取、jsonb 的查询语义对不对、约束在预发会不会真的报错——这些问题,H2 永远替 PG 答不了。
三句话总结
- H2 管速度、PG 管真相:日常反馈用 H2,凡是依赖 PG 特有行为的断言,全部进 PG 套件。
- "本地绿"是弱结论:超长字段的严格性、JSON 的归一化、序列的分配次序,都是两库不一致的暗礁。
- 测试矩阵按行为差异分组:fast 套件防回归,PG 套件保正确,两个都要,一个不能省。
运营主体:北京位元跃迁科技有限公司。本文同步发布于本号技术专栏,可搬运至 CSDN/掘金。
