你好,伙计!
今天,让我们谈谈数据库迁移是什么,以及它们为何如此重要。在当今世界,任何对数据库的更改都应谨慎进行,并按照特定流程进行。理想情况下,这些步骤应该集成到我们的 CI/CD 流水线中,以便一切都可以自动运行。
下面是我们的议程:
- 问题出在哪里?
- 如何解决?
- 一个简单的例子
- 一个更复杂的例子
- 建议
- 结果
- 结论
问题出在哪里?
如果你的团队从未处理过数据库迁移,也不太确定为什么需要它们,那么让我们来解决这个问题。如果你已经了解基础知识,可以跳过这部分。
主要挑战
当我们对数据库进行“计划内”和“平稳”的更改时,我们需要保持服务可用性,并满足 SLA 要求(以便用户不会遭受停机或延迟)。想象一下,你想要在拥有 500 万用户的表中更改列类型。如果你直接进行这个操作(例如,简单运行 ALTER TABLE
而不进行准备),这个表可能会被锁定很长一段时间 — 这会导致用户无法使用服务。
为了避免这样的麻烦,请遵循两条规则:
- 以不锁定表的方式应用迁移(或至少最小化锁定)。
- 如果你需要改变列的类型,通常更容易的方法是先创建一个具有正确类型的新列,然后再删除旧列。
另一个问题:版本控制和回滚
有时你需要回滚一个迁移。
手动操作这个过程 — 进入生产数据库并调整数据 — 不仅风险较大,而且如果没有直接访问权限的话,这样做可能根本不可能。这就是专门的迁移工具派上用场的地方。它们让你能够干净地应用更改并在需要时撤销它们。
我们如何解决这个问题?使用正确的工具
每种语言和生态系统都有自己的迁移工具:
- 对于Java,Liquibase或Flyway很常见。
- 对于Go,一个受欢迎的选择是goose(我们将在这里看一下)。
- 等等。
Goose:它是什么以及为什么有用
Goose是一个轻量级的Go实用程序,可以帮助你自动管理迁移。它提供:
- 简单性。最小的依赖关系和迁移的透明文件结构。
- 多功能性。支持各种数据库驱动程序(PostgreSQL、MySQL、SQLite等)。
- 灵活性。可以使用SQL或Go代码编写迁移。
安装Goose
go install github.com/pressly/goose/v3/cmd/goose@latest
工作原理:迁移结构
默认情况下,Goose会在db/migrations
目录中查找迁移文件。每个迁移遵循以下格式:
NNN_migration_name.(sql|go)
NNN
是迁移编号(例如,001
,002
等)。- 之后,可以是任何描述性名称,例如
init_schema
。 - 扩展名可以是
.sql
或.go
。
SQL迁移示例
文件:001_init_schema.sql
:
-- +goose Up
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);
-- +goose Down
DROP TABLE users;
我们的第一个示例
更改列类型(字符串 → 整数)
假设我们有一个users
表,其中有一个类型为VARCHAR(255)
的age
列。现在我们想将其更改为INTEGER
。迁移可能如下所示(文件005_change_column_type.sql
):
-- +goose Up
ALTER TABLE users ALTER COLUMN age TYPE INTEGER USING (age::INTEGER);
-- +goose Down
ALTER TABLE users ALTER COLUMN age TYPE VARCHAR(255) USING (age::TEXT);
这里发生了什么:
-
向上迁移
- 我们将
age
列更改为INTEGER
。USING (age::INTEGER)
子句告诉PostgreSQL如何将现有数据转换为新类型。 - 请注意,如果
age
中有任何非数字数据,此迁移将失败。在这种情况下,您将需要更复杂的策略(请参阅下文)。
- 我们将
-
向下迁移
- 如果我们回滚,则将
age
恢复为VARCHAR(255)
。 - 我们再次使用
USING (age::TEXT)
将INTEGER
转换回文本。
- 如果我们回滚,则将
第二种复杂情况:多步迁移
如果age
列可能包含混乱数据(不仅仅是数字),最好分几步来处理:
- 添加一个名为
age_int
的新列,类型为INTEGER
。 - 将有效数据复制到新列中,处理或删除无效条目。
- 删除旧列。
-- +goose Up
-- Step 1: Add a new column
ALTER TABLE users ADD COLUMN age_int INTEGER;
-- Step 2: Try to move data over
UPDATE users
SET age_int = CASE
WHEN age ~ '^[0-9]+$' THEN age::INTEGER
ELSE NULL
END;
-- (optional) remove rows where data couldn’t be converted
-- DELETE FROM users WHERE age_int IS NULL;
-- Step 3: Drop the old column
ALTER TABLE users DROP COLUMN age;
-- +goose Down
-- Step 1: Recreate the old column
ALTER TABLE users ADD COLUMN age VARCHAR(255);
-- Step 2: Copy data back
UPDATE users
SET age = age_int::TEXT;
-- Step 3: Drop the new column
ALTER TABLE users DROP COLUMN age_int;
为了允许正确的回滚,Down
部分只需按相反顺序执行相同的操作。
自动化是关键
为节省时间,将迁移命令添加到Makefile(或任何其他构建系统)中会非常方便。以下是一个带有主要Goose命令的示例Makefile,适用于PostgreSQL。
让我们假设:
- 数据库的DSN为
postgres://user:password@localhost:5432/dbname?sslmode=disable
。 - 迁移文件位于
db/migrations
。
# File: Makefile
DB_DSN = "postgres://user:password@localhost:5432/dbname?sslmode=disable"
MIGRATIONS_DIR = db/migrations
# Install Goose (run once)
install-goose:
go install github.com/pressly/goose/v3/cmd/goose@latest
# Create a new SQL migration file
new-migration:
ifndef NAME
$(error Usage: make new-migration NAME=your_migration_name)
endif
goose -dir $(MIGRATIONS_DIR) create $(NAME) sql
# Apply all pending migrations
migrate-up:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) up
# Roll back the last migration
migrate-down:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) down
# Roll back all migrations (be careful in production!)
migrate-reset:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) reset
# Check migration status
migrate-status:
goose -dir $(MIGRATIONS_DIR) postgres $(DB_DSN) status
如何使用?
1. 创建新迁移(SQL文件)。这将生成一个文件db/migrations/002_add_orders_table.sql
。
make new-migration NAME=add_orders_table
2. 应用所有迁移。Goose将在您的数据库中创建一个schema_migrations
表(如果尚不存在),并按升序应用任何新迁移。
make migrate-up
3. 回滚最后一个迁移。只撤销最后一个。
make migrate-down
4. 回滚所有迁移(在生产环境中谨慎使用)。完全重置。
make migrate-reset
5. 检查迁移状态。
make migrate-status
输出示例:
$ goose status
$ Applied At Migration
$ =======================================
$ Sun Jan 6 11:25:03 2013 -- 001_basics.sql
$ Sun Jan 6 11:25:03 2013 -- 002_next.sql
$ Pending -- 003_and_again.go
总结
通过使用迁移工具和Makefile,我们可以:
- 限制直接访问生产数据库,只能通过迁移进行更改。
- 轻松跟踪数据库版本并在出现问题时回滚。
- 保持单一、一致的数据库更改历史记录。
- 执行“平滑”迁移,不会在微服务世界中破坏正在运行的生产环境。
- 获得额外验证 — 每个更改都将经历PR和代码审查过程(假设您已经设置了这些设置)。
另一个优点是很容易将所有这些命令集成到您的CI/CD流程中。并记住 — 安全第一。
例如:
jobs
migrate
runs-on ubuntu-latest
steps
name Install Goose
run
make install-goose
name Run database migrations
env
DB_DSN $ secrets.DATABASE_URL
run
make migrate-up
结论和建议
主要思想很简单:
- 保持迁移的规模小而频繁。这样更容易审核、测试,必要时还可以回滚。
- 在所有环境中使用相同的工具,以确保开发、测试和生产环境保持同步。
- 将迁移集成到 CI/CD 中,这样就不会依赖于任何人手动运行迁移。
通过这种方式,您将拥有一个可靠且受控的更改数据库结构的流程 — 这样不会影响生产环境,并且可以在出现问题时快速响应。
祝您在迁移中一切顺利!
感谢阅读!
Source:
https://dzone.com/articles/goose-as-crucial-tool-for-your-service