migrate使用

sql-migrate支持哪些数据库?

sql-migrate库支持哪些类型的数据库呢?库里面构建了一个map[string]gorp.Dialect来维护。
下面是源码部分,比如我们业务的数据库要用postgres,它是支持的。

var MigrationDialects = map[string]gorp.Dialect{
	"sqlite3":   gorp.SqliteDialect{},
	"postgres":  gorp.PostgresDialect{},
	"mysql":     gorp.MySQLDialect{Engine: "InnoDB", Encoding: "UTF8"},
	"mssql":     gorp.SqlServerDialect{},
	"oci8":      OracleDialect{},
	"godror":    OracleDialect{},
	"snowflake": gorp.SnowflakeDialect{},
}

sql-migrate自身数据存哪里?

这个migrate三方库需要记录下历史执行过的数据库操作,这样每次项目启动再次执行
migrate的时候,可以扫描历史执行过的任务记录,在根据提供的任务清单能区分出新任务。
在源码中已经定义了一个结构体,它对应sql-migrate用到的表。

type MigrationRecord struct {
   Id        string    `db:"id"`
   AppliedAt time.Time `db:"applied_at"`
}

sql-migrate是通过使用gorp库来进行数据库操作的,当使用migrate的时候,会创建一个gorp.DbMap,
这个MigrationRecord结构体对象就是作为gorp的一个参数, 以便知道操作什么表。

// Create migration database map
dbMap := &gorp.DbMap{Db: db, Dialect: d}
table := dbMap.AddTableWithNameAndSchema(MigrationRecord{}, ms.SchemaName, ms.getTableName()).SetKeys(false, "Id”)
// …
err := dbMap.CreateTablesIfNotExists()

上面的逻辑其实都在下面方法中,func (ms MigrationSet) getMigrationDbMap(db *sql.DB, dialect string) (*gorp.DbMap, error),即提供一个db连接,一个数据库类型标示,就能得到一个gorp.DbMap。
这里会检验表是否存在表,不存在就创建。

关于这个表,它是MigrateSet的属性,提供两个get() 、set()方法,默认的名字就是【gorp_migrations】

func (ms MigrationSet) getTableName() string {
   if ms.TableName == "" {
      return "gorp_migrations"
   }
   return ms.TableName
}

这个表gorp_migrations中就存了已经执行过的任务,记录一个任务唯一id,还有任务执行的时间。每次使用migration来执行up或者down时,都是基于这个表来进行判断的。

sql-migrate数据的几个提供方式说明

给migrate提供数据,可以在业务代码中硬编码写好sql,也可以独立创建一些sql文件。
在库中定义了一个接口MigrationSource,然后提供了几个实现。

  • MemoryMigrationSource, 用于硬编码方式提供sql。
  • FileMigrationSource,用于文件方式提供sql。
  • AssetMigrationSource,
  • EmbedFileSystemMigrationSource,
  • HttpFileSystemMigrationSource,
  • PacketMigrationSource,

实现这个接口MigrationSource,就是要实现FindMigrations() ([]*Migration, error)

type MigrationSource interface {
   // Finds the migrations.
   //
   // The resulting slice of migrations should be sorted by Id.
   FindMigrations() ([]*Migration, error)
}

我们用了不同的MigrationSource实现,都会待用这个FindMigrations方法来的到一个Migration组成的切片,用于后续的执行。

MemoryMigrationSource方式使用例子和说明

用这个的话,就是代码编写直接用migrage.MemoryMigrationSource构建要执行的sql。

package main

import (
   "fmt"
   "github.com/jmoiron/sqlx"
   _ "github.com/lib/pq" // required for SQL access
   "github.com/rubenv/sql-migrate"
)

func main() {
   host := "127.0.0.1"
   port := 25432
   dbName := "learn"
   user := "admin"
   pass := "admin"
   SSLMode := "disable"
   SSLCert := ""
   SSLKey := ""
   SSLRootCert := ""

   url := fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s " +
      "sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s",
      host, port, user, dbName, pass,
      SSLMode, SSLCert, SSLKey, SSLRootCert)

   db, err := sqlx.Open("postgres", url)
   if err != nil {
      panic(err)
   }

   source := migrate.MemoryMigrationSource{
      Migrations: []*migrate.Migration{
         {
            Id: "20230329",
            Up: []string{
               `
                  CREATE TABLE IF NOT EXISTS demo
                  (
                     id serial primary key,
                     name character varying(100) NOT NULL,
                     detail character varying (500) not null default '',
                     score int not null default 0,
                     created bigint DEFAULT 0
                  );
                  comment on column demo.id is '自增主键';
                  comment on column demo.name is '名称';
                  comment on column demo.created is '创建时间';
                  comment on column demo.detail is '详情';
                  comment on column demo.score is '分数';
               `,
            },
            Down:                   nil,
            DisableTransactionUp:   false,
            DisableTransactionDown: false,
         },
      },
   }

   _, err = migrate.Exec(db.DB, "postgres", source, migrate.Up)
   if err != nil {
      panic(err)
   }
}

代码硬编码了sql,处理时,为了不影响并发情况,把[ ]*Migration进行copy。
库中构建了一个类型 type byId [ ]*Migration,实现了sort接口,用来对这个切片排序的。

FindMigration( )逻辑:

  • 对于MemoryMigrationSource来说,它的FindMigration()非常简单。
  • 把我们硬编码提供的[ ]*Migration, 按照byId类型实现的排序接口功能和Migration的less(), 进行排序。

Migration中less()排序逻辑:

  • migrate都是按照其id进行排序的。
  • 通过正则去拿id之前的数字,然后进行比较,逻辑如下。
    • 两个migration对象如果其id都是数字前缀,并且不想等,就按照数字的大小排序。
    • 如果a是数字前缀,b不是,a和b比较,less()返回true,即a小。
    • 如果a不是数字前缀,b是,a和b比较,less()返回false,即b小。
    • 如果a数字,b数子,a=b,a小。
    • 如果a非数字,b非数字,a小。
  • 白话翻译下,即a和b比较(这里a、b代表两个要进行比较的migrate对象),a、b都是数字前缀,就按照大小,如果有非数字的,数字的小。都不是数字,就按照切片中的顺序,a和b比a小。

FileMigrationSource方式使用例子和说明

需要提供一个待代码的路径,比如 ”./“,就是main.go所在的路径。
扫描这个目录下所有以".sql"为结尾的文件。
把一个文件转为一个Migration对象

type Migration struct {
	Id   string
	Up   []string
	Down []string

	DisableTransactionUp   bool
	DisableTransactionDown bool
}

这个对象的id,就是用文件的名称,比如 【20230403-1338.sql】,这个名字是我提供的文件名,用日期命名能天然确保顺序性、唯一性。

文件格式说明

sql文件中为了标识是up还是down,提供了一个特殊的开始字符标识,这就是 ”-- +migrate “。
比如文件的第一行如下

-- +migrate Up

程序读取到一行字符串以”-- +migrate “开头,就解析他,按照空格分,就会被分为命令部分和参数部分,上面的参数就是”Up“。

sql文件解析的详细解析

对文件一行行读取并分析是命令提示行(up、down、state)还是注释,还是sql语句,然后把解析到的sql存到对应的切片中,上面ParseMigration结构体维护了两个切片,分别存up的语句和down的语句

	UpStatements   []string
	DownStatements []string

核心逻辑就在下面的方法中

func ParseMigration(r io.ReadSeeker) (*ParsedMigration, error) {

文件例子,xxxx.sql, 解析程序就会读取下面的sql生成一个ParsedMigration对象。

-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE IF NOT EXISTS demo
(
    id serial primary key,
    name character varying(100) NOT NULL,
    detail character varying (500) not null default '',
    score int not null default 0,
    created bigint DEFAULT 0
);
comment on column demo.id is '自增主键';
comment on column demo.name is '名称';
comment on column demo.created is '创建时间';
comment on column demo.detail is '详情';
comment on column demo.score is '分数';

-- +migrate Down
DROP TABLE public.demo;

currentDirection变量

  • 是用来控制读取到的每一个独立sql是存到Up的字符串切片还是Down的字符串切片。
  • 我们sql例子中,一个sql文件中有Up命令符和Down命令符。这样当读取到Up命令符的时候会把currentDirection变为Up,这样后续读取到sql都存到Up的字符串切片。同理读取到Down命令符会把currentDirection变为Down。
  • 所以基于这个变量currentDirection和命令符-- +migrate Up-- +migrate Down的支持,一个sql中可以把Up和Down的sql都维护了。

注释行

  • 遇到 “— ”,但不是“— +”直接忽略,因为这是代表注释行,不是约定的指令行。

事务机制

  • ParsedMigration结构体提供了两个bool属性用于分别控制Up和Down的事务。
  type ParsedMigration struct {
  	UpStatements   []string
  	DownStatements []string
  
  	DisableTransactionUp   bool
  	DisableTransactionDown bool
  }
  • DisableTransactionUp、DisableTransactionDown默认false,代表Up的语句和Down的语句执行时都是非事务的。

  • 当读取到-- +migrate Up notransaction时候,发现命令后面带了notransaction时就把变量DisableTransactionUp设置为true。

  • 当读取到-- +migrate Down notransaction时候,发现命令后面带了notransaction时就把变量DisableTransactionDown设置为true。

  • 注意点,对文件扫描时,如果一个sql文件中有多个Up命令,任何一个Up命令带上notransaction, 整个文件扫描得到的Up的sql都持有同样的结果,即“非事务”,对于Down命令同理。所以对于一个sql文件里面如果要维护Up命令的一批sql,一个文件就有一个Up足够。如有有多个Up,文件可读性很差容易误解。例如下面sql文件内容,故意写成两组Up命令,看似上面一组默认事务的,下面一组非事务,其实只要这个sql文件带一个Up notransaction,整个文件所有Up的sql语句都是事务的。我们故意把其中两个insert语句写错,即本来2个字段提供3个值。执行的时候,下面第一个sql会插入成功,然后剩下三个都不行,因为整体Up的sql都是非事务的。

-- +migrate Up
-- SQL in section 'Up' is executed when this migration is applied
Insert into demo(name, detail) values ('John', 'detail');
Insert into demo(name, detail) values ('Justin', 'detail', '111');

-- +migrate Up notransaction
-- SQL in section 'Up' is executed when this migration is applied
Insert into demo(name, detail) values ('Tom', 'detail');
Insert into demo(name, detail) values ('Jenny', 'detail', '111');
  • 不论是默认的事务形式,还是配置为非事务,只要执行出现错误,最后在gorp_migrations表中并不记录,这个要额外注意,这时候要人为介入了。sql-migration是认为这个任务没有执行的,下次还会执行。

解析文件核心过程说明

  • ParseMigration()方法就是解析sql文件的核心逻辑所在。
  • currentDirection 默认为directionNone, 等读取到了命令行Up或Down,才会修改其值,变为directionUp或者directionDown.
  • 读取一行数据,如果是注释行,继续读下一行。

  • 读取一行数据,如果是命令行,解析出名命令符和参数,比如 Up和notransaction。

    • 如果是Up,就设置currentDirection=directUp。
    • 如果还带有参数notransaction,就设置PargeMigration对象的DisableTransactionUp属性为true。(这里就要注意,如果sql文件中有多个Up,有的带notransaction有的不带,但是有一个带了就代表全局的,所以一个sql文件就最好有一个Up,省的误解)
  • 分隔符

    • 默认使用分号“;”作为分隔符,即ignoreSemicolons=false。我们也能设置自己的分隔符,LineSeparator=“”.
    • 三个条件,不忽略分号、设置行分隔符、读取的内容正好等于行分隔符,满足三个说明当前是“行分隔符”。
isLineSeparator := !ignoreSemicolons && len(LineSeparator) > 0 && line == LineSeparator
  • 读取的行,如果判断不是“行分隔”,就把内容读取到buf中。说明是一个还没结束的sql部分,都存起来后面统一放到字符串切片中。
  • 什么时候把buf的内容存到UpStatements或者DownStatements切片中呢?
    • 两个条件,1、不忽略分号,2、且当前读取的行是以分号结尾的。如果满足就进入到下面if中。根据当前属于什么指令下(Up或者Down)来把读取到buf中的字符串追加到对应的切片中,buf再重置。
    • 如果上面两个条件不满足,整个扫描文件的循环继续,即继续读取下一行,说明读到的sql还不完整。
    • 比如一个create table的语句是很多行组成的,需要多次行读取,每次都存到buf中,等到有分号了判定结束(比如有一行读取到了");"),最后把这个完成buf存到对应Up或Down的字符串切片中,buf再重置。
		if (!ignoreSemicolons && (endsWithSemicolon(line) || isLineSeparator)) || statementEnded {
			statementEnded = false
			switch currentDirection {
			case directionUp:
				p.UpStatements = append(p.UpStatements, buf.String())

			case directionDown:
				p.DownStatements = append(p.DownStatements, buf.String())

			default:
				panic("impossible state")
			}

			buf.Reset()
		}
  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值