上一章执行的 select 1 不依赖业务表,因此只要 PostgreSQL 可以连接就能成功。要把 HealthStatus 真正保存到数据库,还要先定义数据放在哪张表、每一列允许保存什么,以及应用启动时怎样创建这些结构。
本章先用 Spring Boot 自带的 SQL 初始化机制创建 health_status 表。它适合学习和结构简单的应用;当表结构需要长期演进时,应改用带版本记录的数据库迁移工具。
1. 从数据库到一行数据
理解建表语句之前,先分清几个容易混在一起的概念:
| 概念 | 在当前项目中的实例 | 作用 |
|---|---|---|
| 数据库 | spring | 保存一组相关的数据对象 |
| schema | public | 在数据库内部组织表等对象的命名空间 |
| 表 | health_status | 保存结构相同的健康状态记录 |
| 列 | service、status、checked_at | 规定每个属性的名称和数据类型 |
| 行 | spring / UP / 2026-09-04 10:00:00+08 | 一条完整的业务记录 |
PostgreSQL 连接 URL 中的最后一段 spring 指向数据库。没有显式指定 schema 时,普通配置通常会在 public schema 中查找表,因此完整名称也可以写成 public.health_status。
一张表不是 Java 类文件。Java 中的 HealthStatus record 负责应用运行时的数据表达,数据库表负责持久保存;两者通过 Repository 中的 SQL 和结果映射建立对应关系。
2. 定义表结构
在 src/main/resources 下创建 schema.sql:
1create table if not exists health_status (2service varchar(50) primary key,3status varchar(20) not null,4checked_at timestamptz not null default current_timestamp5);
这条 DDL 语句定义了三列:
| 列 | 含义 | 约束 |
|---|---|---|
service | 服务名称,最多 50 个字符 | 主键,不能为空且不能重复 |
status | 当前状态,最多 20 个字符 | NOT NULL,必须提供值 |
checked_at | 最近一次检查时间 | 默认使用数据库当前时间,且不能为空 |
当前需求是「每个服务只保存一份最新状态」,所以直接把 service 作为主键。同一服务再次上报时应该更新原来的行,而不是插入第二行。以后如果需要保留每一次检查的历史记录,应增加历史表,而不是取消这张表的唯一性。
timestamptz 是 PostgreSQL 的 timestamp with time zone。它表示一个确定的时间点,输出时会根据数据库会话时区显示;列本身不会保存最初输入的时区名称。对跨时区运行的服务,它通常比不带时区的 timestamp 更合适。
default current_timestamp 只在插入时没有提供 checked_at 时生效。默认值不是自动更新时间:以后执行 UPDATE 时,仍然需要在 SQL 中明确设置新的检查时间。
3. 添加初始数据
schema.sql 负责结构,data.sql 负责初始数据。在相同目录创建:
1insert into health_status (service, status)2values ('spring', 'UP')3on conflict (service) do nothing;
插入语句没有提供 checked_at,所以数据库使用列的默认值。ON CONFLICT (service) DO NOTHING 表示主键已经存在时跳过这条插入,应用重复启动不会因为相同的初始记录而失败。
初始数据应当只包含应用运行所必需的稳定数据。不断变化的业务数据应该由 Service 和 Repository 写入,而不应该靠修改 data.sql 管理。
4. 启用 SQL 初始化
Spring Boot 会从 classpath 根目录查找 schema.sql 和 data.sql,打包前它们对应项目中的 src/main/resources。对于 PostgreSQL 这样的外部数据库,脚本初始化默认不启用,需要在配置中明确设置:
1spring:2sql:3init:4mode: always5encoding: UTF-8
启动顺序是先执行 schema.sql,再执行 data.sql。默认情况下,只要任意语句执行失败,Spring Boot 就会停止应用启动。这比忽略错误后让应用连接到残缺的表结构更容易定位问题,因此不要为了绕过报错随意开启 continue-on-error。
如果文件需要放在其他位置,可以使用 spring.sql.init.schema-locations 和 spring.sql.init.data-locations 指定 classpath 路径。当前项目沿用默认名称即可,不需要增加额外配置。
完成后的资源目录如下:
1src/main/resources/2├── application.yaml3├── data.sql4└── schema.sql
5. 启动并检查结果
先确认 PostgreSQL 容器正在运行,再提供数据库密码启动应用:
1docker compose up -d2DB_PASSWORD='spring' ./mvnw spring-boot:run
应用启动成功后,可以进入 PostgreSQL 查看表结构和数据:
1docker compose exec postgres psql -U spring -d spring
在 psql 中执行:
1\d health_status23select service, status, checked_at4from health_status5order by service;
\d health_status 显示列、数据类型和约束,SELECT 应该能读到 spring 服务的初始状态。退出 psql 使用 \q。
如果应用提示认证失败,应回到数据库连接配置检查用户名、密码和环境变量;如果提示关系不存在,应确认 schema.sql 位于 src/main/resources,并且 spring.sql.init.mode 已设为 always;如果建表脚本执行失败,应优先阅读异常中最靠近 PostgreSQL 的错误信息,而不是开启忽略错误。
6. 初始化不是结构迁移
IF NOT EXISTS 只会在表完全不存在时创建它。假设以后在 schema.sql 中增加一列,而数据库里已经存在旧表,PostgreSQL 不会重新执行 CREATE TABLE 的列定义,旧表也不会自动得到新列。
因此,这种脚本适合从空数据库启动的学习项目、原型或测试环境,但不能完整记录一个长期运行数据库的演进过程。正式项目通常使用 Flyway 或 Liquibase,把每次结构变更保存为按顺序执行、可追踪的迁移。
一个项目应当选择一种主要的表结构管理机制。以后引入迁移工具时,应让迁移工具负责建表和修改表结构,同时关闭这里的脚本初始化,避免两套机制同时操作相同对象。
现在数据库已经有了稳定的表结构。下一章会实现 PostgresHealthStatusRepository,用 JdbcClient 将 HealthStatus 写入这张表并重新查询出来。