【Go Web开发】使用SQL迁移

在上一篇文章中已经安装了迁移工具,让我们通过在数据库中创建一个新的movies表来演示如何使用迁移工具。

首先使用migrate create创建一对迁移文件。跟随本文在终端中运行以下命令:

$ migrate create -seq -ext=.sql -dir=./migrations create_movies_table
/Users/wangmingjun/Projects/greenlight/migrations/000001_create_movies_table.up.sql
/Users/wangmingjun/Projects/greenlight/migrations/000001_create_movies_table.down.sql

在这个命令中:

  • -seq命令行参数表示:我们想要使用顺序编号为迁移文件命名,比如0001,0002...(而不是Unix时间戳,这是默认值)。
  • -ext命令行参数表示:迁移文件使用.sql扩展名。
  • -dir表示:创建的迁移文件存放目录。(如果目录不存在就会自动创建)
  • create_movies_table是一个描述性的标签,便于识别迁移文件的内容。

现在进入migrations目录,你将看到一对新建"up"和“down”迁移文件:

./migrations/
├── 000001_create_movies_table.down.sql 
└── 000001_create_movies_table.up.sql

此时这两个文件还是空的。下面我们编辑'up'迁移文件,添加CREATE TABLE语句来创建movies表。如下所示:

File: imgrations/000001_create_movies_table.up.sql


CREATE TABLE IF NOT EXISTS movies (
id bigserial PRIMARY KEY,
created_at timestamp(0) with time zone NOT NULL DEFAULT NOW(), title text NOT NULL,
year integer NOT NULL,
runtime integer NOT NULL,
genres text[] NOT NULL,
version integer NOT NULL DEFAULT 1
);

注意这个表中的字段和类型与我们之前创建的Movie结构中的字段和类型相似吗?这很重要,因为这意味着我们可以轻松地将movies表中的每一行数据映射到Go代码中的单个Movie结构体。

如果您对上面SQL语句中的不同PostgreSQL数据类型不熟悉,可以查看官方文档,上面提供了全面的概述。但需要指出的最重要的事情是:

  • id列类似为bigserial它是一个64位自增整数,从1开始是表的主键。
  • genres列类型为text[],是一个包含零个或多个text值数组。需要注意的是,PostgreSQL中的数组本身是可查询和可索引的,这一点我们将在本书的后面进行演示。
  • 在可能的情况下,在每个列上设置NOT NULL约束和适当的默认值是最简单的——就像我们上面所做的那样。
  • 对于存储字符串,我们使用文本类型,而不是varchar或varchar(n)类型。如果你感兴趣,这篇博文解释了为什么文本通常是PostgreSQL中使用的最佳字符类型。

好了,让我们继续“down”迁移,并添加对应的SQL语句来回退我们刚刚编写的“up”迁移。

File: migrations/000001_create_movies_table.down.sql


DROP TABLE IF EXISTS movies;

在PostgreSQL中,DROP TABLE命令是删除表中存在的所有索引和约束,所以这一条语句就足以逆转上面对“up”操作。

ok,前面是我们准备好的第一对迁移文件!

下面,让我们创建第二对含CHECK约束的迁移文件,在数据库级别上强制执行一些业务规则。具体来说,我们希望确保runtime值总是大于零,year值在1888和当前年份之间,并且genres数组总是包含1到5个条目。

执行以下命令来创建第二对迁移文件:

$ migrate create -seq -ext=.sql -dir=./migrations add_movies_check_constrains

/Users/wangmingjun/Projects/greenlight/migrations/000002_add_movies_check_constrains.up.sql
/Users/wangmingjun/Projects/greenlight/migrations/000002_add_movies_check_constrains.down.sql

然后添加以下SQL语句,分别添加和删除CHECK约束:

File: migrations/000002_add_movies_check_constraints.up.sql


ALTER TABLE movies ADD CONSTRAINT movies_runtime_check CHECK (runtime >= 0);
ALTER TABLE movies ADD CONSTRAINT movies_year_check CHECK (year BETWEEN 1888 AND date_part('year', now()));
ALTER TABLE movies ADD CONSTRAINT genres_length_check CHECK (array_length(genres, 1) BETWEEN 1 AND 5);

File: File: migrations/000002_add_movies_check_constraints.down.sql


ALTER TABLE movies DROP CONSTRAINT IF EXISTS movies_runtime_check;
ALTER TABLE movies DROP CONSTRAINT IF EXISTS movies_year_check;
ALTER TABLE movies DROP CONSTRAINT IF EXISTS genres_check;

当我们在movies表中插入或更新数据时,如果这些检查失败,我们的数据库驱动程序将返回类似这样的错误:

pq: new row for relation "movies" violates check constraint "movies_year_check"

注意:单个迁移文件包含多个SQL语句是完全可以的,正如我们在上面的两个文件中看到的那样。事实上,我们本可以在第一对迁移文件中包含CHECK约束和CREATE TABLE语句,但是出于演示目的,将它们放在单独的迁移中可以帮助我们说明迁移工具是如何工作的。

执行迁移

现在我们准备在greenlight数据库上运行两个“up”迁移。

如果您跟随前面的步骤操作,那么使用下面的命令执行迁移,并从您的环境变量传入数据库DSN。如果一切都配置正确,您应该看到一些输出,确认迁移已经成功执行。类似于:

$ migrate -path=./migrations -database=$GREENLIGHT_DB_DSN up
1/u create_movies_table (48.079834ms)
2/u add_movies_check_constrains (71.984417ms)

此时,你使用psql连接到数据库,并使用\dt命令列出刚创建对表:

$ psql $GREENLIGHT_DB_DSN
psql (13.4)
Type "help" for help.

greenlight=> \dt
                 List of relations
 Schema |       Name        |   Type   |   Owner  
--------+-------------------+----------+------------
 public | movies            | table    | greenlight
 public | schema_migrations | table    | greenlight
(2 rows)

您应该看到已经创建了movies表和schema_migrations表,这两个表都由greenlight用户创建。

schema_migrations表是由迁移工具自动生成的,用于跟踪哪些迁移已经生效。让我们快速浏览一下它的表内容:

greenlight=> select * from schema_migrations;
 version | dirty 
---------+-------
       2 | f
(1 row)

这里的version列表明已经在数据库上执行了(包括)序列文件中的第2个迁移文件。dirty列的值为false,这表明迁移文件被干净地执行,没有任何错误,其中包含的SQL语句全部成功地执行。

如果您愿意,还可以在movies表上运行\d命令查看表结构,并确认CHECK约束已正确创建。像这样:

greenlight=> \d movies;
                                        Table "public.movies"
  Column   |            Type             | Collation | Nullable |              Default       
-----------+-----------------------------+-----------+----------+------------------------------------
 id        | bigint                      |           | not null | nextval('movies_id_seq'::regclass)
 create_at | timestamp(0) with time zone |           | not null | now()
 title     | text                        |           | not null | 
 year      | integer                     |           | not null | 
 runtime   | integer                     |           | not null | 
 genres    | text[]                      |           | not null | 
 version   | integer                     |           | not null | 1
Indexes:
    "movies_pkey" PRIMARY KEY, btree (id)
Check constraints:
    "genres_length_check" CHECK (array_length(genres, 1) >= 1 AND array_length(genres, 1) <= 5)
    "movies_runtime_check" CHECK (runtime >= 0)
    "movies_year_check" CHECK (year >= 1888 AND year::double precision <= date_part('year'::text, now()))

附加内容

迁移到指定版本

查看schema_migrations表的另一种方法:如果您想查看您的数据库当前处于哪个迁移版本,您可以运行迁移工具的version命令,如下所示:

$ migrate -path=./migrations -database=$GREENLIGHT_DB_DSN version
2

你也可以使用goto命令向上或向下迁移到一个特定的版本:

$ greenlight migrate -path=./migrations -database=$GREENLIGHT_DB_DSN goto 1 
2/d add_movies_check_constrains (41.570125ms)
$ greenlight migrate -path=./migrations -database=$GREENLIGHT_DB_DSN version
1
执行回退

可以使用down命令回滚特定版本的迁移。例如,要回滚最近的迁移,你需要运行:

 $ migrate -path=./migrations -database =$EXAMPLE_DSN down 1

就我个人而言,我通常更喜欢使用goto命令来执行回滚(因为它对目标版本更明确),并保留使用down命令来回滚所有迁移,如下所示:

$ migrate -path=./migrations -database=$GREENLIGHT_DB_DSN down
Are you sure you want to apply all down migrations? [y/N]
y
Applying all down migrations
2/d add_movies_check_constrains (27.182625ms)
1/d create_movies_table (37.182834ms)

另一种方法是使用drop命令,它将从数据库中删除所有表,包括schema_migrations表——但是数据库本身以及已经创建的序列和枚举等其他内容将保留下来。因此,使用drop可能会使数据库处于混乱和未知的状态,如果您想要回滚所有内容,通常最好使用down命令。

修改SQL迁移错误

理解在SQL迁移文件中出现语法错误时会发生什么是很重要的,因为迁移工具可能会导致一些令人费解的结果。

当运行有错误的迁移时,该错误后续所有SQL语句都会影响,然后迁移工具将退出并打印错误信息。类似于:

$ migrate -path=./migrations -database=$EXAMPLE_DSN up
1/u create_foo_table (36.6328ms)
2/u create_bar_table (71.835442ms)
error: migration failed: syntax error at end of input in line 0: CREATE TABLE (details: pq: syntax error at end of input)

如果迁移失败的文件包含多个SQL语句,那么可能是在遇到错误之前部分SQL操作已经生效。反过来,就迁移工具而言,这意味着数据库处于未知状态。

因此,schema_migrations字段中的version字段将包含失败迁移的编号,dirty字段将被设置为true。此时,如果你运行另一个迁移(即使是“向下”迁移),你将会看到类似这样的错误消息:

 Dirty database version {X}. Fix and force version.

您需要做的是检查错误原因,并确定失败的迁移文件是否部分已经生效。如果是,那么您需要手动回滚部分生效的迁移。

完成之后,还必须将schema_migrations表中的版本号“强制”设置为正确的值。例如,要设置数据库版本号为1,你应该像这样使用force命令:

 $ migrate -path=./migrations -database=$EXAMPLE_DSN force 1

一旦您强制设置版本,数据库被认为是“干净的”,您应该能够再次运行迁移而没有任何问题。

远程迁移文件

migrate工具还支持从远程源(包括Amazon S3和GitHub存储库)读取迁移文件。例如:

$ migrate -source="s3:///" -database=$EXAMPLE_DSN up
$ migrate -source="github://owner/repo/path#ref" -database=$EXAMPLE_DSN up
$ migrate -source="github://user:personal-access-token@owner/repo/path#ref" -database=$EXAMPLE_DSN up

关于此功能的更多信息和支持的远程sources的完整列表可以在这里找到。

应用启动时执行迁移

如果您愿意,也可以使用golang-migrate/migrate Go包(不是命令行工具)在应用程序启动时自动执行数据库迁移。

在本书中我们不使用这种方法,所以如果您遵循本文,请不要更改您的代码。但大致上代码是这样的:

package main

...

func main() {

   ...

    db, err := openDB(cfg)
    if err != nil {
        logger.Fatal(err, nil)
    }
    defer db.Close()

    logger.Info("database connection pool established", nil)

    migrationDriver, err := postgres.WithInstance(db, &postgres.Config{})
    if err != nil {
        logger.Fatal(err, nil)
    }

    migrator, err := migrate.NewWithDatabaseInstance("file:///path/to/your/migrations", "postgres", migrationDriver)
    if err != nil {
        logger.Fatal(err, nil)
    }

    err = migrator.up()
    if err != nil && err != migrator.ErrNoChange {
        logger.Fatal(err, nil)
    }

    logger.Printf("database migrations apply")
}

虽然这是可行的(看起来很有吸引力),但是从长远来看,迁移的执行与应用程序源代码的紧密耦合可能会产生限制和问题。

将数据库迁移与服务器启动分离这篇文章对此进行了很好的讨论,如果您感兴趣,我建议您阅读这篇文章。它以Python语言为主,但不要因此而却步——同样的原则也适用于Go应用程序。

你可能感兴趣的:(【Go Web开发】使用SQL迁移)