10 KiB
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 定案,不再重议)
- 一次票全清:建单一事实源 + harness,12 个测试类一次性全部迁入,
mvn test全绿收口。不分批——12 处改动同构机械、零语义变化,164 跑全绿回归网现成;分批留下「基建建了只有 3 个用户」的中间态。 - 载体 = Java 常量类
CustomerH2Schema(List<String> SCHEMA,16 张表全量、H2 方言 text block,全库建齐、库名按类保留独立):不落schema.sql——全工程零src/test/resources/ 零.sql资源先例,且现状物理形态就是List.of("""…""")常量,收敛是零解析零 IO 零新机制的搬运;schema.sql 要手写语句分割器 = 「解析器比数据还厚」的新浅模块。与候选 #1「注解 SQL + String 常量」载体纪律一以贯之。 - harness 形态 = 组合式静态工厂:
CustomerH2Harness.bootstrap(dbName, Class<?>... mappers)返回SqlSessionFactory,每类@BeforeAll缩成一行。不抽基类(JUnit 5 static@BeforeAll无多态,绕法@TestInstance(PER_CLASS)全工程零先例);不写自定义 Extension(25 处@ExtendWith全是 Mockito 官方,自定义零先例)。interface = 一个静态方法,实现(DDL 常量 + 配置 + 拦截器 + 建表执行)全部隐藏。 - 跨域表全收:16 张表统一进
CustomerH2Schema(客户域 13 + 跨域 3),跨域表段落注释「投影自 crm-opportunity / crm-preference,结构变更须同步」——生产 SQL 早已跨域 join,DDL 收进 harness 是事实投影不是新耦合;警示从CustomerMapperjavadoc 挪到真正会被改的地方。 - 对账测试
CustomerH2SchemaConsistencyTest:先只对 customer 主表(55+ 列最大头、D-05 教训所在地),反射实体字段(@Column(name=...)优先,规避孤立大写字母陷阱;驼峰转蛇形兜底;BaseEntity 审计列白名单)+ 解析 schema 常量列名,断言双向一致。与候选 #1 守恒断言同一哲学:约定变可执行契约。 - 收尾:
crm-customer/CONTEXT.md补一句「H2 测试 schema 单一事实源」;五决策载体理由写进CustomerH2Schema/CustomerH2Harness类 javadoc,不新开 ADR(模块内部实现纪律,够不上跨模块架构决策,与候选 #1 同构)。
做法
- 新建
CustomerH2Schema(test 源码集):16 张表 DDL 常量,基准 = 11 份同构版 customer(52 列)+ 各类子表并集;3 张跨域表带同步警示注释;类 javadoc 写载体决策理由(为何常量类非 schema.sql、为何收跨域表)。 - 新建
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)。只管自举,不管清理——@BeforeEachDELETE 清单语义归各测试类保留。 - 12 个测试类逐一迁移:删各自
@BeforeAll自举块(DS 字段 / configuration / 拦截器 / GlobalConfigUtils / try 建表 /BOOTSTRAPPED标志)+ 内联SCHEMA常量,换一行CustomerH2Harness.bootstrap("库名", Mapper…);库名、mapper 清单、@BeforeEach/@AfterEach原样保留(语义零变化)。 - 新建
CustomerH2SchemaConsistencyTest:customer 主表实体 ↔ DDL 双向对账(口径见拍板 5)。 crm-customer/CONTEXT.md测试相关段落补语(内容见拍板 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 跑全绿(语义零变化的证明);
CustomerWorkspaceSavedViewIntegrationTest8 跑绿——迁到 52 列全量 schema 后无用例转红,风险注记的上报条件未触发(未暴露对 30 列旧世界的语义依赖,历史假绿确认为「测不到」而非「测错」);CustomerWorkspaceOwnershipIntegrationTest21 跑绿——候选 #1 的 4 个守恒用例在 harness 自举下依然成立(两票叠加无冲突);- 新增
CustomerH2SchemaConsistencyTest1 跑绿。
收尾核验
- 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,非新库)。