TT Lab
开始
学习 学习路径 课程

Spring Boot — 数一数发了几条查询

一个列表页跑 301 条查询

在 TT Lab 中继续学习

目标

看看 Spring 替你做的事。校验、异常转换、事务边界、延迟加载, 全都发生在看不见的地方,所以问题也看不见。

尤其是最后一项——绘制一个列表要发出多少条查询—— 要亲自数一数。

环境

Java 21 · Maven · Spring Boot 3.4.1 · H2(内存)。没有网络。

mkdir -p /root/work/spring
cp -r /opt/lab/samples/spring-starter/* /root/work/spring/
cd /root/work/spring
mvn -o -B test          # -o 를 빠뜨리면 원격을 보러 가서 실패합니다

依赖只能使用骨架 pom.xml 中已有的——web、validation、 data-jpa、actuator、h2、test。无法下载新的依赖。

契约

评分器不会引用你的类,只通过 HTTP 和查询数量来检查。 不管怎么拆分类,只要下面这些符合即可。

路径 契约
GET /healthz {"status":"ok"}
POST /items {name, price} · 正常 200/201 · 违反约束 400
GET /items 已保存的列表(JSON 数组)
GET /items/{id} 存在则 200 · 不存在则 404 + 消息
POST /items/bulk 数组 · 只要有一条失败就全部回滚
GET /shops 每家店铺的商品数 · 查询不超过 2 条

包名请使用 labhub。

步骤

  1. 骨架 · /healthz
  2. @Valid → 400
  3. @RestControllerAdvice → 404
  4. 构造器注入
  5. @Transactional 回滚
  6. 统计并减少 N+1 → 06-queries.txt
  7. @WebMvcTest 切片
  8. 总结 → 08-notes.md

参考

评分器会把项目复制一份再检查。 你的 src 中不会留下评分用的文件。

搭建项目

在 /root/work/spring 中创建 Spring Boot 项目,让 GET /healthz 返回 {"status":"ok"}。包名为 labhub。

骨架已经准备好:mkdir -p /root/work/spring && cp -r /opt/lab/samples/spring-starter/* /root/work/spring/。没有网络,所以始终要加上 -o(离线):mvn -o -B test。无法下载骨架 pom 中没有的依赖。

把校验交给框架

创建 POST /items,并用 @Valid 校验请求体。名称为空或价格为负数时必须返回 400,正常时返回 200/201。

record ItemRequest(@NotBlank String name, @Min(1) int price)。如果不加 @Valid,约束注解什么都不会做——加了却没有触发的情况,大多就是这个原因。如果你在写 if (name == null),那就是抢了框架的活。

在一个地方把异常转换为状态码

让 GET /items/{id} 对不存在的 id 返回 404 和一条消息。处理逻辑统一收拢到 @RestControllerAdvice 这一处。

如果在每个控制器里分散写 try-catch,就没有人知道哪个异常会以什么状态码返回。不能用 return ResponseEntity.ok(Map.of("error", ...)) 返回 200——状态码是契约,代理和监控都是依据它来运作的。

改为构造器注入

设置 @Service 层,不要在字段上加 @Autowired。只使用构造器注入(private final)。

只有一个构造器时,根本不需要 @Autowired。好处有:可以声明为 final,测试中可以直接传入并替换,依赖有几个一目了然,循环引用会在启动时暴露。这比在运行中暴露要好。

批量失败就全部撤回

通过 POST /items/bulk 接收数组并保存。只要有一条错误就必须全部回滚,全部正常则必须全部保存。

把 @Transactional 加在服务方法上。注意三点——受检异常默认不会回滚(需要 rollbackFor),在同一个类中用 this.method() 调用不会经过代理而被忽略,private 方法上加也没有用。

统计并减少查询

把店铺和商品设为 1:N,让 GET /shops 同时返回每家店铺的商品数。测量查询数量,将修改前和修改后的结果写入 06-queries.txt,最终要让查询数不超过 2 条。

在 application.properties 中加入 spring.jpa.properties.hibernate.generate_statistics=true,并在测试中用 Statistics.getPrepareStatementCount() 来统计。先用 findAll() 实现,确认有多少家店铺就会多发出多少条查询,然后用 @Query("select distinct s from Shop s join fetch s.items") 把它减少到 1 条。知道怎么统计,比知道怎么修复更重要。

只启动 Web 层的测试

用 @WebMvcTest 写一个只验证控制器的测试。服务用 @MockitoBean 代替注入。

@WebMvcTest 不会启动服务 Bean,所以不注入 Mock 的话上下文无法启动。请用 MockMvc 来调用。如果每次都启动完整的上下文,测试会变慢,一慢就没有人去运行了。

三个看不见的东西

在 08-notes.md 中写至少三行。为什么要用构造器注入,N+1 为什么会产生、用什么修复,@Transactional 会撤回什么(以及不会撤回的情况)。

正文中必须包含 생성자、N+1、롤백(韩文,依次意为“构造器”“N+1”“回滚”)。把第 6 步的两个数字也一并写下来,以后再看到同样的代码时,判断会更快。