用 golang 实现 migration 管理工具

golang-migration

在 PHP 领域,Laravel 的 migration 是挺好用的,通过命令去创建 migration、升级和回滚数据库、初始化等。

但在 golang 里面,我暂时没发现有哪个 migration 工具像 Laravel 那样,像 GORM 这个功能强大的 DB 库,其 migration 的功能也略显简单,不过 GORM 的重心是在 ORM 方面。

然而,用 golang 实现一个 migration 工具,其实也十分简单的,我们可以模拟 laravel 的 migration 来实现它。下面我将说说用 golang 来实现 migration 工具的思路和部分代码。

如果想直接查看源码的朋友,可以看这里:fengzifz/migration-go

思路

先了解 Laravel migration 的几个常用命令:

创建 migration:

1
2
3
4
5
6
7
8
9
10
11
12
# 创建 user 表
php artisan make:migration create_users_table

# 更新 user 表
php artisan make:migration add_age_column_for_users_table

# 更新数据库
php artisan migrate

# 回滚数据库
php artisan rollback
php artisan rollback –step=2

上面两个命令运行之后,对应的 migration 的内容是不太一样的,因为 Laravel 提供了两种模板文件,如果 migration 的名字包含 create_ 前缀和 _table 后缀,那么程序会自动把中间的当成表的名字,调用创建表的 migration 模板来生成 migration 文件。否则,就是用另一种模板来创建。

按照上面的套路,我们用 golang 这样去实现:

  1. 准备基本的 SQL 创建表的模板;
  2. 根据命令,选择对应的模板,去生成对应的 migration 文件;
  3. 编写升级和回滚的函数。

下面是用 golang 实现

1. 编写 SQL 模板
1
2
3
4
5
6

createTableSql := “CREATE TABLE DummyTable (\n” +
“id int(10) UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, \n” +
“created_at timestamp NULL DEFAULT NULL, \n” +
“updated_at timestamp NULL DEFAULT NULL\n” +
“);”

到时可以根据用户的命令行输入的 migration 的名字,来替换上述字符串中的 DummyTable

2. 创建 migration 文件

这里用一种比较基本的方式来创建 migration 管理文件。我们根据用户输入的 migration 文字,加上时间作为前缀,创建一个目录,在目录里面创建一个 up.sql 和 down.sql 文件,分别表示升级和回滚。

1
2
3
4
5
6
# 目录结构
database
|- migrations
|- 20190419084915_create_user_fields_table
|- up.sql
|- down.sql

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
func CreateMigration(name string) (string, error) {

var (
err error
upFile *os.File
downFile *os.File
)

timestamp := time.Now().Format(“20060102150405”)
str := []string{migrationPath, timestamp, “_”, name}
dirName := strings.Join(str, “”)
createDir(dirName)

// Match table creation
// use CreateMigration.stub template for table creation
// use blank.stub template for others
reg := regexp.MustCompile(`^create_(\w+)_table$`)

upFile, err = os.Create(dirName + “/up.sql”)
if err != nil {
return “”, err
}

downFile, err = os.Create(dirName + “/down.sql”)
if err != nil {
return “”, err
}

defer upFile.Close()
defer downFile.Close()

upWriter := bufio.NewWriter(upFile)
downWriter := bufio.NewWriter(downFile)

if reg.MatchString(name) {
r := strings.NewReplacer(“create_”, “”, “_table”, “”)
tableName := r.Replace(name)
_, err = upWriter.WriteString(strings.Replace(createTableSql, “DummyTable”, tableName, -1))
if err != nil {
return “”, err
}

upWriter.Flush()

_, err = downWriter.WriteString(strings.Replace(dropTableSql, “DummyTable”, tableName, -1))
if err != nil {
return “”, err
}

downWriter.Flush()
} else {
_, err = upWriter.WriteString(“”)
if err != nil {
return “”, err
}

_, err = downWriter.WriteString(“”)
if err != nil {
return “”, err
}
}

color.Green(“Created: %v”, name)
return dirName, nil
}
3. 升级和回滚
  • 升级:读取 /database/migrations 目录下面的所有子目录名字,然后从时间最近的一个目录开始,一个个地和 migration 表里面的最后一条记录对比,然后决定哪几个版本的 migration 需要升级。然后一个个地读取对应 migration 目录里面的 up.sql 的内容,进行升级;
  • 回滚:根据输入的命令的回滚的步数,如果回滚一步,那么就读取最后一条 migration 的记录,获取对应的 migration 目录名字,查找对应的 down.sql 进行回滚。回滚 n 步时同理。

实现代码:

升级 migration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
func Migrate() error {
var (
fSlices []string
arr []string
batch int
files []os.FileInfo
err error
rows *sql.Rows
lastBatch int
dbMigrate []string
toMigrate []string
m *Migration
insertStr string
symbol string
upSql []byte
)

// List migrations files
files, err = ioutil.ReadDir(migrationPath)
if err != nil {
return err
}

for _, f := range files {
arr = strings.Split(f.Name(), “.”)
fSlices = append(fSlices, arr[0])
}

// Check migration version in database
rows, err = db.Query(queryAllMigrationSql)
if err != nil {
return err
}

lastRow := db.QueryRow(queryLastMigrationSql)
lastRow.Scan(&lastBatch)
batch = lastBatch + 1

defer rows.Close()

if lastBatch == 0 {
// No migration record in database, all migrations should to be Migrate
toMigrate = fSlices
} else {
// Get migrated files’ name
for rows.Next() {
m, err = scanRow(rows)
if err != nil {
return err
}

dbMigrate = append(dbMigrate, m.Migration)
}

// Compare and get which migration not migrated yet
for _, v := range fSlices {
if !sliceContain(dbMigrate, v) {
toMigrate = append(toMigrate, v)
}
}
}

// Nothing to Migrate, stop and log fatal
toMigrateLen := len(toMigrate)
if toMigrateLen == 0 {
color.Blue(“Nothing migrated”)
os.Exit(2)
}

// Migrate
for i, v := range toMigrate {

// Read up.sql
upSql, err = ioutil.ReadFile(migrationPath + v + “/up.sql”)
if err != nil {
return err
}

_, err = db.Exec(string(upSql))
if err != nil {
return err
}

color.Green(“Migrated: %v”, v)

// Calculate the batch number, which is need to Migrate
if i+1 == toMigrateLen {
symbol = “”
} else {
symbol = “,”
}

insertStr += “(‘” + v + “‘, ” + strconv.Itoa(batch) + “)” + symbol
}

// Connect sql update statement
updateMigrationSql = strings.Replace(updateMigrationSql, “DummyString”, insertStr, -1)

_, err = db.Exec(updateMigrationSql)
if err != nil {
return err
}

return nil
}

回滚 migration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
func Rollback(step string) error {

var (
lastBatch int
toBatch int
err error
rows *sql.Rows
rollBackMig []string
m *Migration
downSql []byte
)

lastRow := db.QueryRow(queryLastMigrationSql)
lastRow.Scan(&lastBatch)

if i, err := strconv.Atoi(step); err == nil {
if lastBatch >= i {
toBatch = lastBatch – (i – 1)
} else {
color.Red(“Can not Rollback %d steps”, i)
return err
}
}

// Which migrations need to be Rollback
rows, err = db.Query(“SELECT * FROM migrations WHERE `batch`>=” + strconv.Itoa(toBatch))
if err != nil {
return err
}

// Rollback slice
for rows.Next() {
m, err = scanRow(rows)
if err != nil {
return err
}

rollBackMig = append(rollBackMig, m.Migration)
}

// Rolling back
for _, v := range rollBackMig {

downSql, err = ioutil.ReadFile(migrationPath + v + “/down.sql”)
if err != nil {
return err
}

_, err = db.Exec(string(downSql))
if err != nil {
return err
}

color.Green(“Rollback: %s”, v)
}

// Delete migrations record
_, err = db.Exec(“DELETE FROM migrations WHERE `batch`>=” + strconv.Itoa(toBatch))
if err != nil {
return err
}

return nil
}
4. 命令支持

我们在 main 函数里面,读取用户输入的命令,然后再决定执行哪个函数。

在 go 里面,可以用 os.Args 来获取脚本的命令和参数。我们现在对命令做如下约束:

  • 创建 migration:<脚本> make:migration <名字>
  • 升级 migration:<脚本> up
  • 回滚 migration:<脚本> down <步数?>,步数是可选,不填是默认是 1 步

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
func main() {
command := os.Args[1]

if strings.Compare(command, “make:migration”) == 0 {

// ***********************
// CreateMigration a migration file
// ./migrate make xxx
// ***********************

fileName := os.Args[2]

if len(fileName) < 0 {
color.Red(“Please enter a migration file name”)
os.Exit(2)
}

_, err := CreateMigration(fileName)
checkErr(err)

color.Green(“Create migration successfully!”)

} else if strings.Compare(command, “up”) == 0 {

// ****************
// Migrate database
// ./migrate up
// ****************
err := Migrate()
checkErr(err)

color.Green(“Migrate successfully!”)

} else if strings.Compare(command, “down”) == 0 {

// ********
// Rollback
// ./migrate down OR ./migrate down 3
// ********
var step string
if len(os.Args) < 3 {
// Default step is 1
step = “1”
} else {
step = os.Args[2]
}

err := Rollback(step)
checkErr(err)

color.Green(“Rollback successfully!”)

}
}

现在,我们把 migration.go 编译成可执行文件 migration,然后直接运行如下命令:

创建 migration:

1./migrate make:migration create_users_table

结果:

升级 migration:

1./migrate up

结果:

回滚:

1./migrate down

结果:

代码仓库

可以查看我的 github 仓库,查看详细代码:fengzifz/migration-go

You May Also Like

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注