数据库迁移
数据库迁移是一组带版本管理的脚本,用于随时间跟踪数据库模式的变更。迁移不需要你在每个环境中手动执行 SQL 命令,而是确保每一个数据库 —— 开发、预发布、生产 —— 都能自动保持同步。
Finch 会在一个自动创建的 wa_migration 表(每个数据库各一张)中跟踪已应用的迁移,并提供两个相关但不同的应用运行时命令:migrate 用于 MySQL,migrate_sqlite 用于 SQLite。两者都是应用运行时命令 —— 它们运行在你实际的 FinchApp 实例内部,而不是作为顶层的 finch 标志裸调用。这两层命令之间的关系请参阅 Finch CLI。
迁移命令
通过 finch run --args="..." / finch serve --args="..." 运行这些命令,或者直接使用 dart run lib/app.dart ...:
| 命令 | 选项 | 简写 | 描述 |
|---|---|---|---|
migrate |
--init |
-i |
按顺序应用所有待处理的 MySQL 迁移 |
migrate |
--init --sqlite |
在一次调用中同时应用待处理的 MySQL 和 SQLite 迁移 | |
migrate |
--rollback |
-r |
回滚最近一次应用的 MySQL 迁移 |
migrate |
--list |
-l |
列出 MySQL 迁移文件及其应用状态 |
migrate_sqlite |
--init / --rollback / --list |
-i / -r / -l |
相同的操作,但仅作用于 SQLite |
重要提示:
migrate命令上的--sqlite只对--init生效 —— 它会作为一种便利,把两个数据库的待处理迁移一并运行。它会被--rollback和--list忽略;migrate --rollback --sqlite回滚的仍然是一个 MySQL 迁移。要回滚或列出 SQLite 迁移,请始终使用专门的migrate_sqlite命令。
创建一个新的迁移文件(区别于应用某个迁移)则是一个顶层的 finch CLI 命令 —— 参见下方的创建迁移文件。
首次设置
运行 --init 可以将所有迁移应用到一个全新的数据库:
# 仅 MySQL
finch run --args="migrate --init"
# MySQL 和 SQLite 一起
finch run --args="migrate --init --and migrate_sqlite --init"
# 等效地,也可以用一次 migrate 调用完成:
finch run --args="migrate --init --sqlite"
# 仅 SQLite
finch run --args="migrate_sqlite --init"
Finch 会在首次运行时(每个数据库各一次)创建一张 wa_migration 表,用于跟踪哪些文件已经被执行过。之后再调用 --init 时,只会应用尚未运行过的文件。
创建迁移文件
生成一个新的迁移文件是一个顶层的 finch CLI 命令(它不需要应用处于运行状态,因为它只是在搭建文件脚手架):
finch migrate --create --name add_books_table
# 会创建类似这样的文件:migrations/z1700000000000_add_books_table_migration.sql
finch migrate --create --name add_books_table --sqlite
# 会在 pathMigrationSQLite 下创建对应的 SQLite 版本
文件名会带上 z<millisecondsSinceEpoch>_ 前缀(开头的 z 是为了让带时间戳前缀的文件排在任何非迁移文件之后),并对你传入的名称进行 slug 化处理。文件会被放置在 pathMigrationMySQL 目录下(SQLite 则放在 pathMigrationSQLite 目录),两者都在 FinchConfigs 中配置:
FinchConfigs configs = FinchConfigs(
pathMigrationMySQL: pathTo('./migrations'),
pathMigrationSQLite: pathTo('./migrations_sqlite'),
);
--create生成的是一个.sql文件,还是一个现成的 DartDartMigration类样板,取决于你pubspec.yaml中的mysql_migrate.type/sqlite_migrate.type设置(默认值为sql)—— 参见 pubspec.yaml Configuration。
迁移文件格式
每个 .sql 迁移文件包含两个部分:正向迁移(## NEW VERSION)和回滚(## ROLL BACK):
-- 2024-01-15 12:00:00.000
-- MySQL Migration File
-- Name: add_books_table
-- ## NEW VERSION:
CREATE TABLE IF NOT EXISTS `books` (
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`author` VARCHAR(255) NOT NULL,
`published_date` DATE NOT NULL,
`category_id` INT NULL,
PRIMARY KEY (`id`)
);
-- ## ROLL BACK:
DROP TABLE IF EXISTS `books`;
每个迁移(无论是基于文件还是基于 Dart)都运行在一个数据库事务中:如果任何语句失败,事务会自动回滚,该迁移也不会被记录进 wa_migration,因此下一次 --init 会重新拾取修复后的同一份文件。
基于 Dart 的迁移
除了 SQL 文件,你还可以通过继承 DartMigration 用 Dart 定义迁移。使用 addSql() 构建 up()/down() 的 SQL,并设置 target 来选择 MigrationTarget.mysql 或 MigrationTarget.sqlite:
import 'package:finch/mysql.dart';
class M1CreateUsers extends DartMigration {
@override
MigrationTarget get target => MigrationTarget.sqlite;
M1CreateUsers() : super('m1_create_users');
@override
void up() {
addSql('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
);
''');
}
@override
void down() {
addSql('DROP TABLE IF EXISTS users;');
}
}
在 FinchApp 实例上注册迁移。传给 super(...) 的名称在所有已注册的迁移中必须是唯一的 —— 这个名称会被记录进 wa_migration,所以后续重命名它会导致 Finch 认为该迁移从未被应用过:
final app = FinchApp(configs: configs)
..registerDartMigration([
M1CreateUsers(),
M2InsertUsers(),
]);
注册之后,migrate --init/--rollback/--list 以及它们对应的 migrate_sqlite 命令,会在运行迁移目录下任何 .sql 文件的同时,运行匹配该 target 的 Dart 迁移 —— 你可以在同一个项目中自由混用这两种写法。
回滚
# 撤销最近一次应用的 MySQL 迁移
finch run --args="migrate --rollback"
# 撤销最近一次应用的 SQLite 迁移 — 必须使用 migrate_sqlite
finch run --args="migrate_sqlite --rollback"
# 撤销最近两次应用的 MySQL 迁移
finch run --args="migrate --rollback 2"
回滚会执行最近一次应用的文件中 ## ROLL BACK: 部分的 SQL(如果是 Dart 迁移,则执行最近一次应用的迁移的 down() 方法)。
列出迁移状态
finch run --args="migrate --list" # MySQL
finch run --args="migrate_sqlite --list" # SQLite
这会打印一张表,展示每个迁移文件、它是否已经被执行,以及执行时间。
在启动时运行迁移
将迁移作为启动命令的一部分自动运行 —— 这正是示例项目的 Docker 镜像所采用的方式(参见 Docker for Finch):
# 在生产环境启动流程中(例如 Docker 的 CMD)
finch serve --args="migrate --init --and migrate_sqlite --init"
--and 可以在同一个 --args 字符串中串联多个应用运行时命令,因此两个数据库都会在服务器开始处理请求之前完成迁移。
在生产环境中运行迁移之前,请务必先备份你的数据库。