Go 语言编程 — gormigrate GORM 的数据库迁移助手

目录

前言

  • 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 上的需求了。

<div class="post-text" itemprop="text"> <p>When I create my tables in the gorm database, it adds columns to the table that I don't want. I'm not sure how it's adding these extra fields. This causes me to run into an error that says, "pq: null value in column "user_id" violates not-null constraint". "user_id" is the unwanted column that gets added. I'm using gorm and postgreSQL.</p> <p>I have a many to many relationship for my two tables. My first table is created properly and my second table, stores, is created with the provided fields plus two unwanted fields: "user_id" and "stores_id". I've tried removing the many to many relationship to see if that was the issue, I've tried dropping the tables and recreating them with different fields. Regardless, I have not been able to get rid of the two extra columns.</p> <p>The first (working) table:</p> <pre><code>type User struct { gorm.Model ID int `json:"u_id"` Name string `json:"u_name"` Stores []Store `gorm:"many2many:stores;" json:"stores"` } </code></pre> <p>When I execute '\d users', I get the following columns: id, created_at, updated_at, deleted_at, name.</p> <p>The second (problematic) table:</p> <pre><code>type Stores struct { gorm.Model ID int `json:"s_id"` NUM int `gorm:"unique" json:"s_num"` Users []User `gorm:"many2many:user" json:"users"` } </code></pre> <p>When I execute '\d' stores, I get the following columns: user_id, vehicle_id, id, created_at, updated_at, deleted_at, num.</p> <p>I'm executing the creation of these tables through the following code:</p> <pre><code>db.AutoMigrate(&User{}) db.AutoMigrate(&Store{}) </code></pre> <p>On another note, if I add <code>gorm:"primary_key";auto_increment"</code> to my ID values in my structs, I get the error "pq: column "user_id" appears twice in primary key constraint". I was able to get around this by removing the primary_key and auto_increment attributes, running AutoMigrate() and then adding it back in and running AutoMigrate() again - this was totally fine and working.</p> <p>I've also tried manually inserting a user_id and store_id. This works fine, except that I'd have to generate new ones every time because they require uniqueness. I understand that the error "pq: null value in column "user_id" violates not-null constraint" is caused by the fact that I'm not providing a user_id or store_id when I'm creating my store. I'm simply confused why a user_id and store_id column is being generated at all, and I'm hoping I can fix that.</p> </div>
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付 49.00元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值