跳到主内容
创建时间: 2026-09-16最后更新: 2026-09-16

上一章执行的 select 1 不依赖业务表,因此只要 PostgreSQL 可以连接就能成功。要把 HealthStatus 真正保存到数据库,还要先定义数据放在哪张表、每一列允许保存什么,以及应用启动时怎样创建这些结构。

本章先用 Spring Boot 自带的 SQL 初始化机制创建 health_status 表。它适合学习和结构简单的应用;当表结构需要长期演进时,应改用带版本记录的数据库迁移工具。

1. 从数据库到一行数据

理解建表语句之前,先分清几个容易混在一起的概念:

概念在当前项目中的实例作用
数据库spring保存一组相关的数据对象
schemapublic在数据库内部组织表等对象的命名空间
表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:

src/main/resources/schema.sql
1
create table if not exists health_status (
2
service varchar(50) primary key,
3
status varchar(20) not null,
4
checked_at timestamptz not null default current_timestamp
5
);

这条 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 负责初始数据。在相同目录创建:

src/main/resources/data.sql
1
insert into health_status (service, status)
2
values ('spring', 'UP')
3
on 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 这样的外部数据库,脚本初始化默认不启用,需要在配置中明确设置:

src/main/resources/application.yaml
1
spring:
2
sql:
3
init:
4
mode: always
5
encoding: UTF-8

启动顺序是先执行 schema.sql,再执行 data.sql。默认情况下,只要任意语句执行失败,Spring Boot 就会停止应用启动。这比忽略错误后让应用连接到残缺的表结构更容易定位问题,因此不要为了绕过报错随意开启 continue-on-error。

如果文件需要放在其他位置,可以使用 spring.sql.init.schema-locations 和 spring.sql.init.data-locations 指定 classpath 路径。当前项目沿用默认名称即可,不需要增加额外配置。

完成后的资源目录如下:

src/main/resources.txt
1
src/main/resources/
2
├── application.yaml
3
├── data.sql
4
└── schema.sql

5. 启动并检查结果

先确认 PostgreSQL 容器正在运行,再提供数据库密码启动应用:

terminal.bash
1
docker compose up -d
2
DB_PASSWORD='spring' ./mvnw spring-boot:run

应用启动成功后,可以进入 PostgreSQL 查看表结构和数据:

terminal.bash
1
docker compose exec postgres psql -U spring -d spring

在 psql 中执行:

psql.sql
1
\d health_status
2
3
select service, status, checked_at
4
from health_status
5
order 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 写入这张表并重新查询出来。