数据库迁移

数据库迁移是一组带版本管理的脚本,用于随时间跟踪数据库模式的变更。迁移不需要你在每个环境中手动执行 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 文件,还是一个现成的 Dart DartMigration 类样板,取决于你 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 字符串中串联多个应用运行时命令,因此两个数据库都会在服务器开始处理请求之前完成迁移。

在生产环境中运行迁移之前,请务必先备份你的数据库。