56·数据库进阶

数据库迁移与版本管理

migrationgolang-migrateschema

第 56 课 - 数据库迁移与版本管理

学习目标

完成本课学习后,你将能够:

  1. 理解数据库迁移的概念及其在应用开发中的重要性。
  2. 使用 golang-migrate 工具创建、应用和回滚数据库迁移。
  3. 将迁移逻辑集成到 Go 应用程序中,实现自动化的数据库结构管理。
  4. 采用版本控制最佳实践,将迁移脚本纳入项目代码库进行管理。

核心概念

想象一下,你在和一个团队共同开发一个应用。你的本地数据库表结构刚刚增加了新字段,而同事的还是旧版。手动在每个人的机器上执行 ALTER TABLE 语句不仅繁琐,而且极易出错。这就是 数据库迁移 要解决的问题。

数据库迁移 就像是数据库 schema(结构)的 “版本控制” 或 “安装程序”。它是一组按顺序执行的、可追踪的、可重复的脚本,用于对数据库的结构(如表、列、索引)进行增量修改。

为什么需要迁移?

  • 一致性:确保开发、测试、生产环境的数据库结构完全一致。
  • 可追溯性:每一次结构变更都有记录(谁、什么时间、做了什么修改)。
  • 协作友好:新成员克隆代码后,可以一键将数据库升级到最新版本。
  • 回滚安全:如果新的迁移导致问题,可以安全地回退到上一个版本。

golang-migrate 是一个流行的数据库迁移工具,支持多种数据库。我们将使用它的 Go 版本,通过命令行和库调用两种方式来管理迁移。

核心工作流程

  1. 创建迁移文件:生成一对 SQL 文件(或 Go 代码文件),一个用于“上行”(升级),一个用于“下行”(回滚)。
  2. 编写迁移逻辑:在对应的 SQL 文件中编写具体的 CREATE TABLE, ALTER TABLE 等语句。
  3. 应用迁移:按顺序执行所有未执行的“上行”脚本,将数据库升级到最新版本。
  4. 回滚迁移:如果需要,执行“下行”脚本,将数据库回退到指定版本。

代码示例

我们以一个简单的用户管理系统为例,演示如何使用 golang-migrate 管理 PostgreSQL 数据库的结构。

1. 准备工作与安装

首先,安装 golang-migrate 的命令行工具和驱动。

# 安装命令行工具(用于生成和执行迁移文件)
$ go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

# 确保你的 Go 项目中已安装数据库驱动和 migrate 库
$ go get github.com/golang-migrate/migrate/v4
$ go get github.com/golang-migrate/migrate/v4/database/postgres
$ go get _ "github.com/lib/pq" # PostgreSQL 驱动

2. 创建迁移文件

在项目根目录下创建一个存放迁移脚本的文件夹,例如 migrations。然后使用命令行工具创建第一次迁移。

$ mkdir migrations
# 创建一对迁移文件:000001_create_users_table.up.sql 和 000001_create_users_table.down.sql
$ migrate create -ext sql -dir migrations -seq create_users_table

这会生成两个文件:

  • migrations/000001_create_users_table.up.sql
  • migrations/000001_create_users_table.down.sql

3. 编写迁移脚本

000001_create_users_table.up.sql (上行脚本 - 创建表):

CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

000001_create_users_table.down.sql (下行脚本 - 删除表):

DROP TABLE IF EXISTS users;

4. 在 Go 代码中执行迁移

我们编写一个 Go 函数,它在应用启动时自动检查并应用所有待执行的迁移。

// main.go
package main

import (
	"database/sql"
	"fmt"
	"log"

	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file" // 支持文件源
	_ "github.com/lib/pq"
)

// runMigrations 执行数据库迁移
func runMigrations(db *sql.DB) error {
	// 创建 migrate 实例,连接数据库和迁移文件路径
	driver, err := postgres.WithInstance(db, &postgres.Config{})
	if err != nil {
		return fmt.Errorf("创建数据库驱动失败: %w", err)
	}
	m, err := migrate.NewWithDatabaseInstance(
		"file://migrations", // 迁移文件目录,相对于程序运行目录
		"postgres", driver,
	)
	if err != nil {
		return fmt.Errorf("初始化 migrate 失败: %w", err)
	}

	// 执行迁移
	if err := m.Up(); err != nil && err != migrate.ErrNoChange {
		return fmt.Errorf("执行迁移失败: %w", err)
	}

	// 检查迁移版本
	version, dirty, err := m.Version()
	if err != nil && err != migrate.ErrNilVersion {
		log.Printf("获取迁移版本失败: %v", err)
	} else {
		log.Printf("当前数据库版本: %d, 脏状态: %t", version, dirty)
	}
	return nil
}

func main() {
	// 连接数据库(请替换为你的连接信息)
	connStr := "host=localhost port=5432 user=postgres password=yourpassword dbname=testdb sslmode=disable"
	db, err := sql.Open("postgres", connStr)
	if err != nil {
		log.Fatalf("连接数据库失败: %v", err)
	}
	defer db.Close()

	// 测试连接
	if err := db.Ping(); err != nil {
		log.Fatalf("数据库 Ping 失败: %v", err)
	}
	fmt.Println("数据库连接成功!")

	// 执行迁移
	if err := runMigrations(db); err != nil {
		log.Fatalf("迁移过程出错: %v", err)
	}
	fmt.Println("数据库迁移完成!")

	// ... 接下来可以执行你的业务查询 ...
}

5. 创建第二次迁移(演示增量更新)

假设我们需要为 users 表添加一个 is_active 字段。

# 创建第二次迁移
$ migrate create -ext sql -dir migrations -seq add_is_active_to_users

000002_add_is_active_to_users.up.sql:

ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true;

000002_add_is_active_to_users.down.sql:

ALTER TABLE users DROP COLUMN IF EXISTS is_active;

实践练习

练习 1:基础迁移

  1. 创建一个新的迁移,用于创建 posts 表,字段包括 id, user_id (外键), title, content, created_at
  2. 编写完整的上行和下行 SQL 脚本。
  3. 使用 Go 代码应用这个迁移。

预期输出: 成功创建 posts 表,且与 users 表有外键关联。

练习 2:数据迁移

创建一个迁移,为已存在的 users 表中所有记录的 updated_at 字段设置默认值为当前时间。 (提示:你需要使用 UPDATE 语句。确保你的下行脚本能安全回滚此操作。)

预期输出: 所有现有用户的 updated_at 字段被更新为当前时间,或迁移成功回滚。

练习 3:版本管理

  1. 假设当前数据库已经应用了迁移 1 和 2,但版本 2 有问题需要回滚。
  2. 在你的 Go 程序中编写一个函数,允许回滚到指定的版本(例如,回滚到版本 1)。

预期输出: 程序能安全地将数据库版本降级到 1,并删除由版本 2 添加的 is_active 列。

常见错误

  1. 手动修改生产数据库结构:直接在数据库客户端执行 ALTER 语句,而不通过迁移脚本。这会导致团队成员数据库版本不一致,且无法追溯和回滚。

    • 正确做法:所有结构变更都必须通过创建新的迁移脚本来完成。
  2. 迁移脚本包含非幂等操作:例如,在 up 脚本中使用 CREATE TABLE 而不带 IF NOT EXISTS。当脚本因意外重试或重复执行时,会报错。

    • 正确做法:尽量让 SQL 语句具有幂等性(如 CREATE TABLE IF NOT EXISTS, ALTER TABLE ... ADD COLUMN IF NOT EXISTS)。
  3. 迁移文件版本号冲突:两个开发者同时创建了文件名相同(如 000003_xxx.sql)的迁移。

    • 正确做法:使用更精确的时间戳或全局递增的序列生成迁移文件名(migrate create-seq 选项有助于此)。在提交前,始终与主干代码合并并检查 migrations 目录。
  4. 忘记编写对应的 down 脚本:无法回滚错误的迁移,可能导致需要手动修复生产数据库。

    • 正确做法:为每个 up 脚本都编写一个功能对等的 down 脚本,即使它很简单(如 DROP TABLE IF EXISTS)。

小结

本节课我们学习了数据库迁移的核心理念和实践:

  • 迁移是数据库的版本控制:它使数据库结构的变更可追踪、可重复、可协作。
  • golang-migrate 是强大的 Go 工具:我们学会了通过命令行创建迁移文件,并通过 Go 库调用来程序化地应用迁移。
  • 工作流至关重要:遵循“创建迁移 -> 编写 SQL -> 应用迁移”的标准流程,能极大提高开发效率和数据库的稳定性。
  • 最佳实践:将迁移脚本纳入 Git 管理,编写幂等的 SQL,为每个上行脚本配一个下行脚本,避免手动修改数据库。

掌握数据库迁移是成为一名专业后端开发者的必备技能。在下一课中,我们将探索 Go 语言中一个强大的特性:reflect 包。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「反射 reflect」 以巩固所学知识。