Write the Code. Change the World.

11月 26

习惯了 Laravel 的迁移和 seeder,来使用 go 语言。如果没有这些,是不是很不爽。好的就需要模仿和借鉴,当然,有现成省心的更好。网上已经有一些这样的轮子了,这里找一个比较接近 Laravel 的迁移的实现。

github: https://github.com/praveen001/go-db-migration

origin:https://techinscribed.com/create-db-migrations-tool-in-go-from-scratch/

learnku: https://learnku.com/go/t/51228

操作一波

先上个图,证明这个是可以玩起来的,并且和 laravel 中的操作很相似。

第一步: 先建立一个 migrations/migrator.go 文件,这个文件用来执行初始化,创建迁移,执行迁移的直接逻辑。

package migrations

import (
    "bytes"
    "context"
    "database/sql"
    "errors"
    "fmt"
    "html/template"
    "os"
    "time"
)

// Migration ..
type Migration struct {
    Version string
    Up      func(*sql.Tx) error
    Down    func(*sql.Tx) error

    done bool
}

// Migrator ..
type Migrator struct {
    db         *sql.DB
    Versions   []string
    Migrations map[string]*Migration
}

var migrator = &Migrator{
    Versions:   []string{},
    Migrations: map[string]*Migration{},
}

// AddMigration ..
func (m *Migrator) AddMigration(mg *Migration) {
    // Add the migration to the hash with version as key
    m.Migrations[mg.Version] = mg

    // Insert version into versions array using insertion sort
    index := 0
    for index < len(m.Versions) {
        if m.Versions[index] > mg.Version {
            break
        }
        index++
    }

    m.Versions = append(m.Versions, mg.Version)
    copy(m.Versions[index+1:], m.Versions[index:])
    m.Versions[index] = mg.Version
}

// Init ..
func Init(db *sql.DB) (*Migrator, error) {
    migrator.db = db

    // Create `schema_migrations` table to remember which migrations were executed.
    if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
        version varchar(255)
    );`); err != nil {
        fmt.Println("Unable to create `schema_migrations` table", err)
        return migrator, err
    }

    // Find out all the executed migrations
    rows, err := db.Query("SELECT version FROM `schema_migrations`;")
    if err != nil {
        return migrator, err
    }

    defer rows.Close()

    // Mark the migrations as Done if it is already executed
    for rows.Next() {
        var version string
        err := rows.Scan(&version)
        if err != nil {
            return migrator, err
        }

        if migrator.Migrations[version] != nil {
            migrator.Migrations[version].done = true
        }
    }

    return migrator, err
}

// Up ..
func (m *Migrator) Up(step int) error {
    tx, err := m.db.BeginTx(context.TODO(), &sql.TxOptions{})
    if err != nil {
        return err
    }

    count := 0
    for _, v := range m.Versions {
        if step > 0 && count == step {
            break
        }

        mg := m.Migrations[v]

        if mg.done {
            continue
        }

        fmt.Println("Running migration", mg.Version)
        if err := mg.Up(tx); err != nil {
            tx.Rollback()
            return err
        }

        if _, err := tx.Exec("INSERT INTO `schema_migrations` VALUES(?)", mg.Version); err != nil {
            tx.Rollback()
            return err
        }
        fmt.Println("Finished running migration", mg.Version)

        count++
    }

    tx.Commit()

    return nil
}

// Down ..
func (m *Migrator) Down(step int) error {
    tx, err := m.db.BeginTx(context.TODO(), &sql.TxOptions{})
    if err != nil {
        return err
    }

    count := 0
    for _, v := range reverse(m.Versions) {
        if step > 0 && count == step {
            break
        }

        mg := m.Migrations[v]

        if !mg.done {
            continue
        }

        fmt.Println("Reverting Migration", mg.Version)
        if err := mg.Down(tx); err != nil {
            tx.Rollback()
            return err
        }

        if _, err := tx.Exec("DELETE FROM `schema_migrations` WHERE version = ?", mg.Version); err != nil {
            tx.Rollback()
            return err
        }
        fmt.Println("Finished reverting migration", mg.Version)

        count++
    }

    tx.Commit()

    return nil
}

// Status .
func (m *Migrator) MigrationStatus() error {
    for _, v := range m.Versions {
        mg := m.Migrations[v]

        if mg.done {
            fmt.Println(fmt.Sprintf("Migration %s... completed", v))
        } else {
            fmt.Println(fmt.Sprintf("Migration %s... pending", v))
        }
    }

    return nil
}

// Create ..
func Create(name string) error {
    version := time.Now().Format("20060102150405")

    in := struct {
        Version string
        Name    string
    }{
        Version: version,
        Name:    name,
    }

    var out bytes.Buffer

    t := template.Must(template.ParseFiles("./migrations/template.txt"))
    err := t.Execute(&out, in)
    if err != nil {
        return errors.New("Unable to execute template:" + err.Error())
    }

    f, err := os.Create(fmt.Sprintf("./migrations/%s_%s.go", version, name))
    if err != nil {
        return errors.New("Unable to create migration file:" + err.Error())
    }
    defer f.Close()

    if _, err := f.WriteString(out.String()); err != nil {
        return errors.New("Unable to write to migration file:" + err.Error())
    }

    fmt.Println("Generated new migration files...", f.Name())
    return nil
}

func reverse(arr []string) []string {
    for i := 0; i < len(arr)/2; i++ {
        j := len(arr) - i - 1
        arr[i], arr[j] = arr[j], arr[i]
    }
    return arr
}

第二步:创建模板文件,这个是创建迁移的基础。创建的迁移,是以模板文件为基础的。

模板文件, migrations/template.txt

package migrations

import "database/sql"

func init() {
    migrator.AddMigration(&Migration{
        Version: "{{.Version}}",
        Up:      mig_{{.Version}}_{{.Name}}_up,
        Down:    mig_{{.Version}}_{{.Name}}_down,
    })
}

func mig_{{.Version}}_{{.Name}}_up(tx *sql.Tx) error {
    _, err := tx.Exec("Create Table If Not Exists xxx")
    if err != nil {
        return err
    }
    return nil
}

func mig_{{.Version}}_{{.Name}}_down(tx *sql.Tx) error {
    _, err := tx.Exec("DROP TABLE IF EXISTS xxx")
    if err != nil {
        return err
    }
    return nil
}

migrations 中就这两个文件。

第三步:建立 cmd 文件。 一个 cmd/cmd.go,还有一个 cmd/migrate.go 。

# cmd/cmd.go
package cmd

import (
    "log"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "app",
    Short: "Application Description",
}

// Execute ..
func Execute() {
    if err := rootCmd.Execute(); err != nil {
        log.Fatalln(err.Error())
    }
}

还有

# cmd/migrate.go
package cmd

import (
    "fmt"
    "nm/app"
    "nm/migrations"
    "github.com/spf13/cobra"
)

var migrateCmd = &cobra.Command{
    Use:   "migrate",
    Short: "database migrations tool",
    Run: func(cmd *cobra.Command, args []string) {

    },
}

var migrateCreateCmd = &cobra.Command{
    Use:   "create",
    Short: "create a new empty migrations file",
    Run: func(cmd *cobra.Command, args []string) {
        name, err := cmd.Flags().GetString("name")
        if err != nil {
            fmt.Println("Unable to read flag `name`", err.Error())
            return
        }

        if err := migrations.Create(name); err != nil {
            fmt.Println("Unable to create migration", err.Error())
            return
        }
    },
}

var migrateUpCmd = &cobra.Command{
    Use:   "up",
    Short: "run up migrations",
    Run: func(cmd *cobra.Command, args []string) {

        step, err := cmd.Flags().GetInt("step")
        if err != nil {
            fmt.Println("Unable to read flag `step`")
            return
        }

        db := app.NewDB()

        migrator, err := migrations.Init(db)
        if err != nil {
            fmt.Println("Unable to fetch migrator")
            return
        }

        err = migrator.Up(step)
        if err != nil {
            fmt.Println("Unable to run `up` migrations")
            return
        }

    },
}

var migrateDownCmd = &cobra.Command{
    Use:   "down",
    Short: "run down migrations",
    Run: func(cmd *cobra.Command, args []string) {

        step, err := cmd.Flags().GetInt("step")
        if err != nil {
            fmt.Println("Unable to read flag `step`")
            return
        }

        db := app.NewDB()

        migrator, err := migrations.Init(db)
        if err != nil {
            fmt.Println("Unable to fetch migrator")
            return
        }

        err = migrator.Down(step)
        if err != nil {
            fmt.Println("Unable to run `down` migrations")
            return
        }
    },
}

var migrateStatusCmd = &cobra.Command{
    Use:   "status",
    Short: "display status of each migrations",
    Run: func(cmd *cobra.Command, args []string) {
        db := app.NewDB()

        migrator, err := migrations.Init(db)
        if err != nil {
            fmt.Println("Unable to fetch migrator")
            return
        }

        if err := migrator.MigrationStatus(); err != nil {
            fmt.Println("Unable to fetch migration status")
            return
        }

        return
    },
}

func init() {
    // Add "--name" flag to "create" command
    migrateCreateCmd.Flags().StringP("name", "n", "", "Name for the migration")

    // Add "--step" flag to both "up" and "down" command
    migrateUpCmd.Flags().IntP("step", "s", 0, "Number of migrations to execute")
    migrateDownCmd.Flags().IntP("step", "s", 0, "Number of migrations to execute")

    // Add "create", "up" and "down" commands to the "migrate" command
    migrateCmd.AddCommand(migrateUpCmd, migrateDownCmd, migrateCreateCmd, migrateStatusCmd)

    // Add "migrate" command to the root command
    rootCmd.AddCommand(migrateCmd)
}

第四步:生成 sql.DB 的引用。

# app/db.go

package app

import (
    "database/sql"
    "fmt"
    "github.com/go-sql-driver/mysql"
)

// NewDB .
func NewDB() *sql.DB {
    fmt.Println("Connecting to MySQL database...")

    config := mysql.Config{
        User:                 "homestead",
        Passwd:               "secret",
        Addr:                 "127.0.0.1:33060",
        Net:                  "tcp",
        DBName:               "goblog",
        AllowNativePasswords: true,
    }
    fmt.Println(config.FormatDSN())

    // 准备数据库连接池
    db, err := sql.Open("mysql", config.FormatDSN())

    if err != nil {
        fmt.Println("Unable to connect to database", err.Error())
        return nil
    }

    if err := db.Ping(); err != nil {
        fmt.Println("Unable to connect to database", err.Error())
        return nil
    }

    fmt.Println("Database connected!")

    return db
}

第五步:main.go

package main

import (
    "nm/cmd"
)

func main() {
    cmd.Execute()
}

到此,所有文件已经齐全。

执行下边命令,可以让你开心的迁移。

# 创建一个 User 的迁移, 文件名如 20201127135416_User.go
go run main.go migrate create -n User 

# 执行迁移 
go run main.go migrate up

# 回滚迁移
go run main.go migrate down

# 查看状态
go run main.go migrate status

虽然和 laravel 很相似,可和 laravel 相比,相差的太多了。不过,有过总比没有的好。如果乐意,还可以添加 seeder 进来。让你美滋滋。

这里生了一个 user 的迁移文件:

package migrations

import "database/sql"

func init() {
    migrator.AddMigration(&Migration{
        Version: "20201127160251",
        Up:      mig_20201127160251_User_up,
        Down:    mig_20201127160251_User_down,
    })
}

func mig_20201127160251_User_up(tx *sql.Tx) error {
    sql := `Create Table If Not Exists users (
              id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
              name varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
              email varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
              email_verified_at timestamp NULL DEFAULT NULL,
              password varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
              remember_token varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
              created_at timestamp NULL DEFAULT NULL,
              updated_at timestamp NULL DEFAULT NULL,
              PRIMARY KEY (id),
              UNIQUE KEY users_email_unique (email)
            ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`

    _, err := tx.Exec(sql)
    if err != nil {
        return err
    }
    return nil
}

func mig_20201127160251_User_down(tx *sql.Tx) error {
    _, err := tx.Exec("DROP TABLE IF EXISTS users")
    if err != nil {
        return err
    }
    return nil
}

发表评论

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