# 01 · 客户域 H2 测试 schema 收敛(12 份手工副本 → 单一事实源 + 对账看守) - **Status:** resolved - **Type:** task - **来源:** /improve-codebase-architecture 2026-09-06 报告候选 #2(`.scratch/architecture-review-20260906-102629.html`),grilling 六决策已全部拍板 ## 背景(缺陷事实,含对报告的修正) `crm-customer` 12 个 H2 集成测试类各自手搓同构自举(~30 行:JdbcDataSource + URL 模式 + MybatisConfiguration + 分页/乐观锁拦截器 + MetaObjectFillHandler + addMapper + 执行 DDL)+ 各自内联一份 DDL(合计测试代码 6155 行)。四点差异:库名(12 个独立 mem 库)、mapper 注册清单、表集合(每类 2~8 张,并集 **16 张**)、`@BeforeEach` DELETE 清单。 **漂移坐实(比报告更严重)**:11 份 customer DDL 逐列全同构(52 列),唯独 `CustomerWorkspaceSavedViewIntegrationTest` 是 **30 列精简版旧世界,缺 22 列**(报告只点了 `is_biz_negotiated` 1 列;实际还缺 `parent_customer_id` / `legal_representative` / `established_date` / `is_child` 等 21 列,即 D-05 之前的 schema 投影)。至今假绿的机制:MyBatis-Plus insert NOT_NULL 策略跳过 null 字段 + `CustomerMapper` SQL 不引用这 22 列 + 该类造数不设这些字段——它不报警,只是静默测错世界。 **事实修正两则**: - 报告称「lead/opportunity 那边没有内联 DDL 问题」不完全准确——`OpportunityBoardIntegrationTest` 也有 1 份内联 DDL(4 张表)。但单份无重复(rule of three 未触发),不入本票范围。 - 报告方案写「`schema.sql` 落 test/resources」——事实核查后修正为 Java 常量类(见拍板 2 理由)。 **跨域耦合既成事实**:`CustomerMapper` 生产 SQL 的 `opportunity_count` 标量子查询直接 join `opportunity` / `opportunity_customer`(javadoc 已自认「商机表结构变更须同步 H2 SCHEMA」);`user_saved_view` 由 `SavedViewServiceImpl` 直接操作。16 表并集含 3 张跨域表。 **对账基线事实**:Customer 实体 46 个业务字段全部落在 52 列 DDL 内,多出的 6 列 = BaseEntity 审计列(id / creator_id / create_time / updater_id / update_time / deleted)——双向对账当前零漂移,对账测试写完即绿,看守未来。 ## 拍板决策(grilling 定案,不再重议) 1. **一次票全清**:建单一事实源 + harness,12 个测试类一次性全部迁入,`mvn test` 全绿收口。不分批——12 处改动同构机械、零语义变化,164 跑全绿回归网现成;分批留下「基建建了只有 3 个用户」的中间态。 2. **载体 = Java 常量类 `CustomerH2Schema`**(`List SCHEMA`,16 张表全量、H2 方言 text block,全库建齐、库名按类保留独立):**不落 `schema.sql`**——全工程零 `src/test/resources` / 零 `.sql` 资源先例,且现状物理形态就是 `List.of("""…""")` 常量,收敛是零解析零 IO 零新机制的搬运;schema.sql 要手写语句分割器 = 「解析器比数据还厚」的新浅模块。与候选 #1「注解 SQL + String 常量」载体纪律一以贯之。 3. **harness 形态 = 组合式静态工厂**:`CustomerH2Harness.bootstrap(dbName, Class... mappers)` 返回 `SqlSessionFactory`,每类 `@BeforeAll` 缩成一行。**不抽基类**(JUnit 5 static `@BeforeAll` 无多态,绕法 `@TestInstance(PER_CLASS)` 全工程零先例);**不写自定义 Extension**(25 处 `@ExtendWith` 全是 Mockito 官方,自定义零先例)。interface = 一个静态方法,实现(DDL 常量 + 配置 + 拦截器 + 建表执行)全部隐藏。 4. **跨域表全收**:16 张表统一进 `CustomerH2Schema`(客户域 13 + 跨域 3),跨域表段落注释「投影自 crm-opportunity / crm-preference,结构变更须同步」——生产 SQL 早已跨域 join,DDL 收进 harness 是事实投影不是新耦合;警示从 `CustomerMapper` javadoc 挪到真正会被改的地方。 5. **对账测试 `CustomerH2SchemaConsistencyTest`**:先只对 customer 主表(55+ 列最大头、D-05 教训所在地),反射实体字段(`@Column(name=...)` 优先,规避孤立大写字母陷阱;驼峰转蛇形兜底;BaseEntity 审计列白名单)+ 解析 schema 常量列名,断言双向一致。与候选 #1 守恒断言同一哲学:约定变可执行契约。 6. **收尾**:`crm-customer/CONTEXT.md` 补一句「H2 测试 schema 单一事实源」;五决策载体理由写进 `CustomerH2Schema` / `CustomerH2Harness` 类 javadoc,**不新开 ADR**(模块内部实现纪律,够不上跨模块架构决策,与候选 #1 同构)。 ## 做法 1. 新建 `CustomerH2Schema`(test 源码集):16 张表 DDL 常量,基准 = 11 份同构版 customer(52 列)+ 各类子表并集;3 张跨域表带同步警示注释;类 javadoc 写载体决策理由(为何常量类非 schema.sql、为何收跨域表)。 2. 新建 `CustomerH2Harness`(test 源码集):静态工厂 `bootstrap(String dbName, Class... mappers)`,内部持 URL 模式(`MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1`)、MybatisConfiguration、分页/乐观锁拦截器、MetaObjectFillHandler、IdentifierGenerator、执行 `CustomerH2Schema.SCHEMA` 建表;类 javadoc 写形态决策理由(为何组合式非基类/Extension)。**只管自举,不管清理**——`@BeforeEach` DELETE 清单语义归各测试类保留。 3. 12 个测试类逐一迁移:删各自 `@BeforeAll` 自举块(DS 字段 / configuration / 拦截器 / GlobalConfigUtils / try 建表 / `BOOTSTRAPPED` 标志)+ 内联 `SCHEMA` 常量,换一行 `CustomerH2Harness.bootstrap("库名", Mapper…)`;库名、mapper 清单、`@BeforeEach` / `@AfterEach` 原样保留(语义零变化)。 4. 新建 `CustomerH2SchemaConsistencyTest`:customer 主表实体 ↔ DDL 双向对账(口径见拍板 5)。 5. `crm-customer/CONTEXT.md` 测试相关段落补语(内容见拍板 6)。 6. `mvn -pl crm-customer -am test` 全绿 + 全工程 BOM 扫描。 ## 风险注记 - **SavedView 类迁到 52 列全量 schema 后**:其造数不设 22 个缺列、insert NOT_NULL 跳过、SQL 不引用 → 预期仍绿(假绿前提是「列不在场」,全量 schema 下这些列带默认值在场,路径不变)。**若有用例转红 = 暴露了它对旧世界的真实语义依赖**——上报为发现,不悄悄改断言迁就。 - 环境注记:本地仓库 crm-rule 快照过旧,验收命令必须带 `-am`(候选 #1 已记录在案)。 ## 验收 - `mvn -pl crm-customer -am test` 全绿(既有 164 用例 + 4 守恒零回归;新增对账测试绿); - DDL 字面在测试代码里只出现一份(`CustomerH2Schema`),12 个类零内联 DDL、零自举块; - 对账测试绿(= schema 合成忠实性的证明,当前零漂移基线); - 净减行数报数(预期 12 类各删 ~30 行自举 + 100~200 行 DDL,新增 3 文件 ~600 行,净减约 1000+ 行); - BOM 扫描通过(新增 Java 文件无 EF BB BF)。 ## Answer **一次票全清完成,验收 5 条全部满足。** ### 实施内容 **新增 3 个测试基建文件**(`com.crm.customer.testinfra` 包,+509 行): - `CustomerH2Schema`(340 行):16 张表 DDL 单一事实源——customer 主表(52 列)单独暴露 `CUSTOMER_TABLE` 供对账解析;`SCHEMA = List.of(...)` 全量 16 张;跨域表段落带「商机/偏好模块结构变更须同步」警示;类 javadoc 写全六决策载体理由(为何常量类非 schema.sql、为何收跨域表、合成基准、看守机制)。 - `CustomerH2Harness`(96 行):组合式静态工厂 `bootstrap(dbName, Class... mappers)` + `dataSourceOf(factory)` 便捷方法(`NgramNameSimilarityPortImpl` 直连用点);建表段 SQLException 转 IllegalStateException,调用方 `@BeforeAll` 无需 throws。 - `CustomerH2SchemaConsistencyTest`(73 行):customer 主表实体 ↔ DDL **严格双向**列集对账——实体侧并入 `BaseEntity` 父类字段(6 审计列),`@Column(name=...)` 显式优先、驼峰转蛇形兜底;断言两侧互为子集(= 相等)。 **迁移 12 个测试类**(+189 / -1673 行,净 -1484):各删 `@BeforeAll` 自举块(DS 字段 / MybatisConfiguration / 拦截器 / GlobalConfigUtils / 建表 try 块 / BOOTSTRAPPED 标志)+ 内联 SCHEMA 常量,换 1~4 行 `CustomerH2Harness.bootstrap("原库名", 原mapper清单)`;库名、mapper 清单、`@BeforeEach` DELETE 清理、全部业务断言原样保留(语义零变化)。 **CONTEXT.md**:Language 区新增「H2 测试 schema」词条(单一事实源 + 自举入口 + 对账看守 + 跨域同步警示 + _Avoid_)。 ### 忠实性证据(机器验证,非目测) - 合成校验:生成文件的 16 张表 DDL 与各基准源逐表规范化对比 **FAITHFUL**(零失真); - 盘点校验:迁移前 12 类的 DDL 变体图谱(customer 11 份同构 + SavedView 30 列漂移版;customer_focus/view_log/follow 三处子表宽松/严格差异及取基准依据); - 对账测试绿:实体 46 字段 + BaseEntity 6 审计列 == DDL 52 列,双向零缺漏——当前零漂移基线成立。 ### 测试证据 `mvn -pl crm-customer -am test`(2026-09-06):**15 个测试类 165 跑 0 失败 0 错误 0 跳过**: - 12 个迁移类合计 154 跑全绿(语义零变化的证明); - `CustomerWorkspaceSavedViewIntegrationTest` 8 跑绿——迁到 52 列全量 schema 后无用例转红,**风险注记的上报条件未触发**(未暴露对 30 列旧世界的语义依赖,历史假绿确认为「测不到」而非「测错」); - `CustomerWorkspaceOwnershipIntegrationTest` 21 跑绿——候选 #1 的 4 个守恒用例在 harness 自举下依然成立(两票叠加无冲突); - 新增 `CustomerH2SchemaConsistencyTest` 1 跑绿。 ### 收尾核验 - BOM 扫描:全工程 719 个 `.java`(排除 target)clean; - 行尾统一:迁移涉及文件统一 CRLF(python 写盘曾产生 LF,已批量转回,与仓库 working copy 一致); - 净变动:12 类净 -1484 行 + 新增 3 文件 +509 行 ≈ **净减 975 行**(票预估 1000+,吻合); - 环境注记:验收命令必须带 `-am`(候选 #1 已记录在案)。 ### 过程注记(实施中发现) - `CustomerServiceImplIntegrationTest` / `CustomerContactServiceIntegrationTest` 在 `@BeforeEach` 把静态 DS 传给 `NgramNameSimilarityPortImpl` 构造器(自举外用点)→ harness 补 `dataSourceOf(SqlSessionFactory)` 静态方法解决(反取同库同实例 DataSource,非新库)。 ## Comments