You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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 列),唯独 CustomerWorkspaceSavedViewIntegrationTest30 列精简版旧世界,缺 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_viewSavedViewServiceImpl 直接操作。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 常量类 CustomerH2SchemaList<String> 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