在 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 这样去实现:
准备基本的 SQL 创建表的模板;
根据命令,选择对应的模板,去生成对应的 migration 文件;
编写升级和回滚的函数。
下面是用 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:
结果:
回滚:
结果:
代码仓库 可以查看我的 github 仓库,查看详细代码:fengzifz/migration-go