Go 語言程式設計 — gormigrate GORM 的資料庫遷移助手

2020-10-12 12:00:34

目錄

前言

  • GORM v2
  • gormigrate v2
  • 程式 Demo:https://github.com/JmilkFan/gormigrate-demo

gormigrate

GORM 本身提供了 AutoMigrate 功能以及 Migrator 提供的 DDL 介面,但 GORM 更加專注於 ORM 層面,所以在 ORM Schema Version Control(資料庫版本控制)有所欠缺。而 gormigrate 就是一個輕量化的 Schema Migration Helper(遷移助手),基於 GORM AutoMigrate 和 Migrator 進行封裝,用於彌補這一塊的缺失。

  • Github:https://github.com/go-gormigrate/gormigrate

需要注意的是,簡潔清理是 gormigrate 最大的優勢也是不足,如果是大型系統有著複雜的版本控制要求,則建議使用 golang-migrate/migrate。

核心結構體

gormigrate 的核心 Struct 是 Gormigrate:

// Gormigrate represents a collection of all migrations of a database schema.
type Gormigrate struct {
    db         *gorm.DB			// DBClient 範例
    tx         *gorm.DB			// 事務型 DBClient 範例
    options    *Options			// migrations 設定選項
    migrations []*Migration		// 版本遷移 Schemas
    initSchema InitSchemaFunc	// 版本初始化 Schemas
}
  • DBClient 範例:最基礎的,也是最通用的 GORM DBClient 範例。

  • 事務型 DBClient 範例:也是 GORM 的 DBClient 範例,區別在於會使用 RDBMS 的 Transaction(事務)特性來執行 GORM 封裝好的 Migrator DDL。注意,是都支援事務性 DBClient 跟 RDBMS 的型別有關。

  • migrations 設定選項:用於設定 migrate 的執行細節。

type Options struct {
    TableName string		// 指定 migrations 版本記錄表的表名,預設為 migrations。
    IDColumnName string		// 指定 migrations 版本記錄表的列名,預設為 id。
    IDColumnSize int		// 指定 migrations 版本記錄表的列屬性,預設為 varchar(255)。
    UseTransaction bool		// 指定是否使用 Transaction 來執行 GORM Migrator DDL。
    ValidateUnknownMigrations bool	// 指定當 migrations 版本記錄表有非法記錄時,是否觸發 ErrUnknownPastMigration 錯誤。
}
  • 版本遷移 Schemas:用於定義版本遷移的 Schemas。
  • 版本初始化 Schemas:用於定義版本初始化的 Schemas。

實現分析

ORM Schema Version Control 需要具備的最基本功能元素:

  • 版本定義
  • 版本記錄(歷史)
  • 版本升級
  • 版本回退

版本定義

正如上文介紹到的,gormigrate 大體上支援兩種型別的版本定義:

  • 版本遷移 Schemas:用於定義版本遷移的 Schemas。
  • 版本初始化 Schemas:用於定義版本初始化的 Schemas。

InitSchema

應用於 init_database_no_table 的場景,可以通過呼叫 Gormigrate 的 InitSchema 方法註冊一個版本初始化 Schemas 函數 「InitSchemaFunc」,並在一個新的乾淨的資料庫中執行它,完成一次全量建表的過程。注意,InitSchema 是沒有 Rollback 的。

函數簽名:

// InitSchemaFunc is the func signature for initializing the schema.
type InitSchemaFunc func(*gorm.DB) error

範例:

type Person struct {
	gorm.Model
	Name string
	Age int
}

type Pet struct {
	gorm.Model
	Name     string
	PersonID int
}

m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
    // you migrations here
})

m.InitSchema(func(tx *gorm.DB) error {
	err := tx.AutoMigrate(
		&Person{},
		&Pet{},
		// all other tables of you app
	)
	if err != nil {
		return err
	}

	if err := tx.Exec("ALTER TABLE pets ADD CONSTRAINT fk_pets_people FOREIGN KEY (person_id) REFERENCES people (id)").Error; err != nil {
		return err
	}
	// all other foreign keys...
	return nil
})

Migration

應用於完成初始化之後的 「增量遷移」 場景,可以通過呼叫 Gormigrate 的 Migration 方法註冊一個 MigrateFunc 和一個 RollbackFunc 函數,前者用於 Upgrade,後者則是對應的 Downgrade,以此來完成 Schema 的升級和回退。

NOTE:當我們使用 InitSchema 和 Migration 方法的時候切記不能使用同一個 Gormigrate 範例,否則會出現只執行 InitSchema 不執行 Migration 的情況,導致資料庫版本無法遷移到 Latest 的現象。因為 InitSchema 在 DDL 建表的時候會把 Migration 的版本記錄都插入到 migrations 表裡面去,但實際上並沒有執行 Migration 的 DDL。

函數簽名:

// MigrateFunc is the func signature for migrating.
type MigrateFunc func(*gorm.DB) error

// RollbackFunc is the func signature for rollbacking.
type RollbackFunc func(*gorm.DB) error

範例:

package main

import (
	"log"

	"github.com/go-gormigrate/gormigrate/v2"
	"gorm.io/gorm"
	_ "github.com/jinzhu/gorm/dialects/sqlite"
)

func main() {
	db, err := gorm.Open("sqlite3", "mydb.sqlite3")
	if err != nil {
		log.Fatal(err)
	}

	db.LogMode(true)

	m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
		// create persons table
		{
			ID: "201608301400",
			Migrate: func(tx *gorm.DB) error {
				// it's a good pratice to copy the struct inside the function,
				// so side effects are prevented if the original struct changes during the time
				type Person struct {
					gorm.Model
					Name string
				}
				return tx.AutoMigrate(&Person{})
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Migrator().DropTable("people")
			},
		},
		// add age column to persons
		{
			ID: "201608301415",
			Migrate: func(tx *gorm.DB) error {
				// when table already exists, it just adds fields as columns
				type Person struct {
					Age int
				}
				return tx.AutoMigrate(&Person{})
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Migrator().DropColumn("people", "age")
			},
		},
		// add pets table
		{
			ID: "201608301430",
			Migrate: func(tx *gorm.DB) error {
				type Pet struct {
					gorm.Model
					Name     string
					PersonID int
				}
				return tx.AutoMigrate(&Pet{})
			},
			Rollback: func(tx *gorm.DB) error {
				return tx.Migrator().DropTable("pets")
			},
		},
	})

	if err = m.Migrate(); err != nil {
		log.Fatalf("Could not migrate: %v", err)
	}
	log.Printf("Migration did run successfully")
}

版本記錄(歷史)

同樣的,gormigrate 也為 InitSchema 和 Migration 提供了兩種版本記錄的方式,前者的 Version ID 寫死為 SCHEMA_INIT,後者的 Version ID 則為自定義的 Migration struct 的 ID 欄位:

// Migration represents a database migration (a modification to be made on the database).
type Migration struct {
    // ID is the migration identifier. Usually a timestamp like "201601021504".
    ID string
    // Migrate is a function that will br executed while running this migration.
    Migrate MigrateFunc
    // Rollback will be executed on rollback. Can be nil.
    Rollback RollbackFunc
}

而 Version History 則是資料庫表 migrations 中的記錄:

test_db=# select * from migrations;
     id
-------------
 SCHEMA_INIT
 v1
 v2
 v3
(4 行記錄)

gormigrate 通過 Version History 記錄來控制版本的升級和回退,如果已經升級完成(存在記錄)的版本則不會被重複執行,否者就會進行全量的初始化或增量的升級。

版本升級和回退

從 Migration 的範例中可以看出,gormigrate 本質上是在封裝 GORM 的 AutoMigrate 和 Migrator DDL 介面的基礎之上實現了版本記錄的功能,所以執行版本升級和回退的實現依舊來自於 GORM 的能力。

對此,筆者在《Go 語言程式設計 — gorm 資料庫版本遷移》已經有過介紹,這裡就不再贅述了。

此外,Gormigrate 還提供了

  • MigrateTo:升級到指定 ID 的版本。
  • RollbackTo:回退到指定 ID 的版本。
  • RollbackLast:撤消上一次遷移。
  • RollbackMigration:執行自定義的回退函數。

通過這些方法,已經可以在一定程度上滿足資料庫應用程式在 ORM Schames Version Control 上的需求了。