数据库迁移工具解决了执行问题,却不一定能表达完整的部署策略。发布流程一旦需要“在某个迁移之后运行检查”“在事务之外创建并发索引”或“只有校验通过才能提交”,团队通常会开始寻找新的参数、钩子或插件。
这些需求背后有一个更基础的事实:迁移工具本质上是一个执行 SQL 的程序。它决定哪些 SQL 运行、运行顺序、是否包在事务中,以及结果能否提交。与其不断等待工具增加配置项,不如反过来,让项目用 SQL 拥有这部分策略。
从工具配置转向项目策略
传统迁移流程通常是:工具扫描文件名,按顺序执行 SQL,再根据自身规则提交事务。文件名能够表达简单顺序,却很难表达完整的数据库语义,例如:
- 某个结构变更完成后,必须立即验证数据完整性;
- 某个索引必须使用
CREATE INDEX CONCURRENTLY,不能放进事务; - 一组迁移需要共享事务,而另一条迁移必须独立提交;
- 即使 SQL 执行成功,只要校验结果不满足条件,整个发布也不能提交。
在这种视角下,迁移工具的配置不是部署策略本身,而只是工具提供的策略语言。工具没有对应的表达能力,就只能提交功能请求。
更灵活的做法是让工具只负责准备一个 PostgreSQL 会话,并把项目中的迁移、阶段、依赖关系和执行状态物化为会话中的关系。之后,由项目拥有的 SQL 程序读取这些关系,选择并排序工作,控制事务边界,执行验证,并决定是否允许提交。
换句话说,工具负责“怎么执行”,项目负责“执行什么以及什么结果才算成功”。
PostgreSQL 会话就是策略运行时
PostgreSQL 适合承载这种部署程序,是因为它不仅能执行数据变更,也能表达检查和控制逻辑:
- 表可以表示待执行的迁移计划和当前状态;
- 查询可以实现排序、依赖选择和条件过滤;
DO块或存储过程可以执行验证逻辑;- 事务可以明确划分提交边界;
RAISE EXCEPTION可以阻止不合格的结果提交;- advisory lock 可以避免两个部署进程同时操作同一个数据库。
这里的关键不是把所有发布系统都塞进数据库,而是把数据库语义交给最了解它的地方。云资源创建、密钥读取、人工审批和流水线权限仍然可以由外部系统负责;数据库程序只处理迁移选择、执行顺序、事务控制和结果验证。
可以把一个部署过程抽象成下面的关系:
project_migrations -> 选择待执行工作
-> 排序并执行 SQL
-> 验证数据库状态
-> 允许提交或抛出异常
一个可改造的最小示例
下面的示例假设迁移工具会在当前 PostgreSQL 会话中准备一张临时表 deploy_migrations。实际工具可以使用不同的表名和字段,但契约可以保持类似:
migration_id:迁移唯一标识;phase:迁移阶段;sql_body:待执行 SQL;transactional:是否允许在事务中执行;applied:是否已经执行。
下面的 SQL 展示了一个事务阶段:它获取部署锁,按项目定义的顺序执行变更,检查约束结果,并在检查失败时通过异常阻止提交。示例中的 deploy_migrations 和迁移内容是可运行的假设数据,便于在测试数据库中改造验证。
-- 运行环境:PostgreSQL 14+。请在专用测试数据库中执行。
BEGIN;
SELECT pg_advisory_xact_lock(hashtextextended('app-schema-deploy', 0));
CREATE TEMP TABLE deploy_migrations (
migration_id text PRIMARY KEY,
phase integer NOT NULL,
sql_body text NOT NULL,
transactional boolean NOT NULL,
applied boolean NOT NULL DEFAULT false
);
INSERT INTO deploy_migrations (migration_id, phase, sql_body, transactional)
VALUES
(
'20250301_add_accounts',
10,
'CREATE TABLE IF NOT EXISTS accounts (
id bigint PRIMARY KEY,
email text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
)',
true
),
(
'20250302_add_email_check',
20,
'ALTER TABLE accounts
ADD CONSTRAINT accounts_email_not_blank
CHECK (length(trim(email)) > 0)',
true
);
DO $deploy$
DECLARE
migration record;
bad_rows bigint;
BEGIN
FOR migration IN
SELECT migration_id, sql_body
FROM deploy_migrations
WHERE NOT applied
AND transactional
ORDER BY phase, migration_id
LOOP
RAISE NOTICE 'applying %', migration.migration_id;
EXECUTE migration.sql_body;
END LOOP;
SELECT count(*)
INTO bad_rows
FROM accounts
WHERE length(trim(email)) = 0;
IF bad_rows > 0 THEN
RAISE EXCEPTION 'deployment validation failed: % invalid accounts', bad_rows;
END IF;
END
$deploy$;
COMMIT;
这个例子只演示机制,不代表所有迁移工具都会接受动态 SQL 或把迁移正文存进关系。生产实现需要补充迁移审计表、幂等规则、失败恢复策略和权限控制。最重要的是,迁移顺序与校验条件已经成为项目代码的一部分,而不是隐藏在某个工具的命令行开关中。
事务边界必须显式建模
CREATE INDEX CONCURRENTLY 是最容易暴露传统迁移模型局限性的例子。它不能在事务块中执行,因此不能简单地放进一个“所有迁移都包在 BEGIN 和 COMMIT 之间”的流程。
可以把部署拆成两个明确阶段:事务阶段负责表结构和约束,自动提交阶段负责并发索引。示例命令如下,运行前请将连接字符串替换成目标数据库连接信息:
#!/usr/bin/env bash
set -euo pipefail
export DATABASE_URL='postgresql://app:secret@localhost:5432/app'
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL'
BEGIN;
SELECT pg_advisory_xact_lock(hashtextextended('app-schema-deploy', 0));
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS normalized_email text;
UPDATE accounts
SET normalized_email = lower(trim(email))
WHERE normalized_email IS NULL;
DO $check$
BEGIN
IF EXISTS (
SELECT 1 FROM accounts
WHERE normalized_email IS NULL OR normalized_email = ''
) THEN
RAISE EXCEPTION 'normalized_email validation failed';
END IF;
END
$check$;
COMMIT;
SQL
# 该命令运行在事务之外,故意保留为独立部署阶段。
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -c \
'CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS accounts_normalized_email_uq
ON accounts (normalized_email);'
如果项目策略规定“并发索引成功后还要再次验证”,就继续把验证写成数据库程序的一部分,并在成功后记录部署状态。不能把并发索引和普通事务变更假设成同一种工作单元,否则失败时的恢复行为会变得含糊。
这种倒置带来的代价
把策略交给项目并不意味着系统自动变简单。成本会从“等待迁移工具支持”转移到项目自身:
- 项目需要维护部署关系的结构和生命周期;
- 团队需要定义幂等、重试、回滚和部分成功的语义;
- 动态执行 SQL 会提高权限设计和审计要求;
- 多个部署进程之间必须使用锁或其他并发控制;
- 迁移程序本身需要测试,不能只测试单个 SQL 文件;
- 运维人员需要理解数据库中的状态,而不只是流水线日志。
还有一个边界必须保持清楚:数据库内的 SQL 程序不能替代云平台审批、密钥服务、发布权限和跨服务编排。它只应该拥有数据库内部的决策权。把外部系统控制逻辑强行搬进 PostgreSQL,通常会扩大故障半径,也会让权限模型更难审计。
采用前的检查清单
可以先从一个需要特殊事务语义的真实迁移开始试验,而不是一次性重写全部发布系统:
- 明确工具会向 PostgreSQL 会话提供哪些关系和字段;
- 将迁移选择、排序和依赖规则写成可测试的 SQL;
- 为事务迁移、自动提交迁移和验证阶段定义清晰边界;
- 使用 advisory lock 防止并发部署;
- 记录每个迁移的状态、执行时间和失败原因;
- 在测试数据库中演练中断、重试和部分完成;
- 保留外部流水线对审批、凭据和云资源的控制。
当部署策略真的属于项目,而不是迁移工具的配置时,工具升级不再决定项目能表达什么。项目可以用 PostgreSQL 自己的关系、事务和异常机制,精确描述哪些工作应该发生,以及什么结果才允许发布完成。