这里写自定义目录标题
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()
}