6.2 代码规范与单元测试 本节摘要:规范管的是"每台手术一个做法"。本节先给一份从真实项目里捞出来的坏味道清单,逐条给改法;再立 Mapper 命名与 XML 书写的基本规矩;最后搭一套基于 H2 内存库的 Mapper 测试骨架,让映射层代码自带安全网。 事务保住了正确性,这一节管可维护性。半年后回来看自己写的 XML 还敢不敢改,取决于今天有没有按规矩做。先看反面教材——这段清单来自一次真实的代码评审: 一份坏味道清单 坏味道 | 危害 | 改法 SQL 里写 | 列一变,resultType 映射悄悄错位或塞回无用大字段 | 明确列出所需列,与 resultMap 对齐 超长 XML(单文件上千行) | 改一处 diff 巨大,冲突高发 | 按业务域拆文件,公共 SQL 用 2.
本节摘要:规范管的是"每台手术一个做法"。本节先给一份从真实项目里捞出来的坏味道清单,逐条给改法;再立 Mapper 命名与 XML 书写的基本规矩;最后搭一套基于 H2 内存库的 Mapper 测试骨架,让映射层代码自带安全网。
事务保住了正确性,这一节管可维护性。半年后回来看自己写的 XML 还敢不敢改,取决于今天有没有按规矩做。先看反面教材——这段清单来自一次真实的代码评审:
| 坏味道 | 危害 | 改法 |
|---|---|---|
SQL 里写 SELECT * |
列一变,resultType 映射悄悄错位或塞回无用大字段 | 明确列出所需列,与 resultMap 对齐 |
| 超长 XML(单文件上千行) | 改一处 diff 巨大,冲突高发 | 按业务域拆文件,公共 SQL 用 2.4 的片段复用 |
| 命名无章法(getUser / queryUser / findUser 混用) | 查 SQL 全靠搜,评审没法快速对上语义 | 全项目统一动词表(见下) |
${} 直接拼用户输入 |
SQL 注入,2.2 讲过的分界线 | 一律 #{};动态表名列名走白名单校验 |
| 环境信息硬编码在 XML | 连接串进版本库,密码泄露 | properties 外置 + 环境变量 |
| 无任何测试 | 改映射全靠上线赌运气 | 本节的 H2 测试骨架兜底 |
select 星号值得单独展开——它在结果映射端有三宗罪:多查的列浪费网络与内存;实体没有的列被静默丢弃,错位了也不报警;加列后"看似没人用"的查询返回结构变化,下游反序列化炸雷。
命名立一张全项目共用的动词表,语义对号入座:
| 动词 | 语义 | 示例 |
|---|---|---|
| select / get | 单条或列表查询 | selectById、getByStatus |
| count | 计数 | countByCreatedDate |
| insert / add | 新增 | insertOrder、addRemark |
| update | 更新(全量或按列) | updateByPrimaryKey、updateStatus |
| delete / remove | 删除 | deleteById、removeExpired |
| batch | 批量变体 | batchInsert、batchUpdateStatus |
XML 书写四条底线:namespace 必须是接口全限定名(绑定靠它,1.5 讲过原理);id 与方法名一一对应;where 条件统一用 4.1 的动态标签收口,手拼 and 串是 bug 温床;复杂 SQL 加注释说明业务含义——SQL 是给人读的第二遍。

不建议直接连测试库跑 Mapper 测试:数据被人改、测试互相污染、CI 环境连不上库,都是雷。H2 内存库加一份建表脚本,让每个测试类都拿到一张干净的新表:
class UserMapperTest { private static SqlSessionFactory factory; private SqlSession session; private UserMapper mapper; @BeforeAll static void initFactory() throws Exception { // mybatis-config 指向 H2 数据源;Mapper XML 照常加载 factory = TestConfig.buildFactory("jdbc:h2:mem:testdb;MODE=MySQL"); } @BeforeEach void setUp() { session = factory.openSession(true); // 测试用 autoCommit,省一层干扰 mapper = session.getMapper(UserMapper.class); SchemaRunner.run(session.getConnection(), "schema.sql"); // 建表 SchemaRunner.run(session.getConnection(), "data.sql"); // 固定测试数据 } @AfterEach void tearDown() throws Exception { session.close(); // mem 库随关闭自动销毁 } @Test void 按状态查询应返回映射完整的订单() { Order db = mapper.selectById(1001L); assertNotNull(db); assertEquals("PAID", db.getStatus()); // 逐字段断言,映射错位当场现形 assertEquals(new BigDecimal("99.00"), db.getAmount()); assertEquals(LocalDateTime.of(2026, 8, 1, 10, 0), db.getCreatedAt()); } @Test void 插入应回填自增主键() { Order o = new Order(); o.setStatus("WAITING"); mapper.insert(o); assertNotNull(o.getId()); // 2.3 的主键回填,测试钉死 assertNotNull(mapper.selectById(o.getId())); } }
断言的两条纪律:逐字段比而不是只比对象不等于空——字段错位只有逐个比对才拦得住;写操作必查回,插入后回读一次,确认真的落库且能查出来。数据清理方面,内存库随会话销毁天然免清理;万一共用实例,就在 @AfterEach 删表,下个测试重建。
⚠️ 常见坑:H2 的 MODE=MySQL 只是方言近似,不是全等——MySQL 特有函数(GROUP_CONCAT 语义差异、JSON 函数)在 H2 上可能报错或结果不同。涉及方言深水区的 SQL,仍需定期在真实库的测试环境做冒烟验证。
💡 关键直觉:Mapper 测试要测的是映射对不对、SQL 对不对,不是业务逻辑——业务层用 Mock 掉 Mapper 来测,映射层用真库(H2)真执行来测。两层测试各守各的边界,混在一起就又慢又脆。
规范立完,下一节看手术太慢怎么办:N 加一的定位与消灭、批量执行的真实收益、大结果集的游标处理。