速读#
Spring Data JPA 的震撼在于继承接口就白送 CRUD,但「白送」也意味着 SQL 细节被框架藏起来。省事可以要,理解缺口不能放着不管;show-sql 是把抽象重新变得可见的第一步。
继承接口,CRUD 全白送#
public interface NoteRepository extends JpaRepository<Note, Long> { List<Note> findByTitleContaining(String keyword);}save、findById、deleteById 现成;连 findByTitleContaining 这种,JPA 也能按方法名生成 SQL——方法名被拆解成 findBy + Title + Containing,翻译成 where title like ?。第一次见挺震撼:我什么 SQL 都没写,活干了。
震撼之后:理解缺口#
「白送」意味着 SQL 是它生成的,不是我写的。不盯生成的 SQL,就不知道实际在干什么。任务完成了,理解留在框架那边。
这种状态我认识:Dockerfile 多阶段构建那次也「能跑不懂」,后来想加缓存优化才发现完全不理解 COPY --from=build 在干嘛。这次没有重蹈:
白送的第一时间就打开 show-sql: true,把生成的 SQL 打出来看。
spring: jpa: hibernate: ddl-auto: update # 开发期自动建表,生产慎用 show-sql: true # 让生成的 SQL 可见(show-sql 是开发期的观察窗;生产上要看 SQL,走日志配置去看,别往标准输出裸打。)
盯着 SQL 看很快就见到了「省事的另一面」:查一批带关联的实体,控制台刷出一长串查询——先一条查询取回主表的 N 行,再为每一行各发一条查询去取关联数据。这就是常说的 N+1:写代码时只是一次 findAll(),跑起来是 1 + N 次查询。根子是两种模型的错配:我在代码里按对象导航,数据库擅长的是按集合一次算完,ORM 在中间默默用多次查询把前者翻译成后者。不开 show-sql,这种事静悄悄发生,数据量小的时候甚至察觉不到——等察觉时往往已经不在开发环境。复杂查询(关联、N+1)不能靠方法名猜,要靠看它生成了什么。
看见之后才有得选:改成一条 join 把关联一次取回,或者接受现状——数据量确实小,优化不划算。选哪个不重要,重要的是**「选」发生在我知情之后**,这就是可见性买回来的东西。
两个「能 ≠ 该」#
| 配置 / 做法 | 方便在哪 | 该不该 |
|---|---|---|
ddl-auto: update | 开发期自动建表 | 生产让框架改表风险高,变更应受控 |
| 密码写进配置文件 | 省事 | 配置进 Git 后密钥收不回来;用 ${DB_PASSWORD} |
ddl-auto 这一个键就值得停一下:update 只加不减,改错的列会一直留着;create 每次启动重建表,数据直接没了。开发期怎么方便怎么来,生产库的表结构变更必须是我审过的 SQL,不是框架启动时的副作用。
工具给的「方便」和工程上的「该不该」是两个问句,得分开答。
JPA vs MyBatis-Plus#
| JPA | MyBatis-Plus | |
|---|---|---|
| 强项 | 简单 CRUD 极致省事 | 复杂查询透明可控 |
| 弱项 | 复杂查询仍要 JPQL,理解缺口大 | 简单场景啰嗦一点 |
我的场景:简单 CRUD 多 + 偶尔复杂查询 → JPA 的省事值钱。复杂查询占大头时,我会更倾向条件构造器那一侧。
抽象不是越深越好#
这条线是从 SQLite 练手一路延续下来的:练手零运维 → 工程化换 MySQL → 再加 ORM。每加一层抽象,多一层理解缺口。
抽象是「省的事」和「藏的细节」之间的跷跷板。 show-sql: true 就是在这头加一个「细节可见」的对冲。
实体长这样时,骨架才算立住:
@Entity@Table(name = "notes")public class Note { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; @Column(columnDefinition = "TEXT") private String body;}带走两个判断:
- 白送的便利,要用「看得见它干了什么」对冲理解缺口
- 能自动 ≠ 该自动;生产配置受控
