版本化架构迁移工具的出现时间远早于声明式对应工具。与单个文件描述数据库架构状态的声明式方法不同,版本化架构迁移由多个文件或脚本组成,这些文件或脚本依次迭代来记录数据库随着时间变化的状态。当对架构进行更改时,会添加新的文件以描述这些更改。这种方法与一个广为熟知的系统非常相似:Git。
迁移文件通常与代码一起存储,并使用第三方工具按需逐步应用到数据库。这些文件通常按照应用顺序编号。系统会在数据库内使用一个专用表以跟踪哪些脚本已经被应用,以及哪些脚本尚未应用。


使用 Laravel 和 Artisan 的示例

下面的示例使用 Laravel 默认的示例应用程序以及 Artisan 命令来执行版本化迁移。当应用程序生成时,项目中会创建一个 database/migrations 文件夹,其中包含一组基础的迁移脚本。

Laravel 示例应用的默认迁移文件

以下是其中一个文件的内容。它使用 PHP 定义表的结构。当 Artisan 读取时,它会被转换为 MySQL 所需的 DDL,以创建相同的表结构。

# 2014_10_12_000000_create_users_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('users');
    }
};

运行以下命令可以创建数据库的基本结构。注意,该文件夹中的所有迁移脚本都会根据文件名顺序逐个运行。

~❯ ./vendor/bin/sail artisan migrate

# 输出:
   INFO  Preparing database.

  Creating migration table .............................. 45ms DONE

   INFO  Running migrations.

  2014_10_12_000000_create_users_table .................. 45ms DONE
  2014_10_12_100000_create_password_resets_table ........ 64ms DONE
  2019_08_19_000000_create_failed_jobs_table ............ 38ms DONE
  2019_12_14_000001_create_personal_access_tokens_table . 44ms DONE

接下来,我们可以查看数据库的结构。注意现在有一个 migrations 表,其中包含每个迁移脚本的名称以及存储在 batch 列中的批次号,以表示 Artisan 已成功运行过这些脚本。

mysql> show tables;
+------------------------+
| Tables_in_example_app  |
+------------------------+
| failed_jobs            |
| migrations             |
| password_resets        |
| personal_access_tokens |
| users                  |
+------------------------+
5 rows in set (0.01 sec)

mysql> select * from migrations;
+----+-------------------------------------------------------+-------+
| id | migration                                             | batch |
+----+-------------------------------------------------------+-------+
|  1 | 2014_10_12_000000_create_users_table                  |     1 |
|  2 | 2014_10_12_100000_create_password_resets_table        |     1 |
|  3 | 2019_08_19_000000_create_failed_jobs_table            |     1 |
|  4 | 2019_12_14_000001_create_personal_access_tokens_table |     1 |
+----+-------------------------------------------------------+-------+
4 rows in set (0.01 sec)

现在,为了升级架构,我们可以运行另一个遵循先前命名约定的迁移脚本。此脚本将在 users 表中添加一个 nickname 列。

# 2023_01_13_000001_add_new_column.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('nickname');
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('nickname');
        });
    }
};

再次运行之前的 migrate 命令。这一次的输出更少,因为只运行了一个脚本。

~❯ ./vendor/bin/sail artisan migrate

   INFO  Running migrations.

  2023_01_13_000001_add_new_column ...................... 32ms DONE

再次查看 migrations 表,可以看到脚本已成功运行。

mysql> select * from migrations;
+----+-------------------------------------------------------+-------+
| id | migration                                             | batch |
+----+-------------------------------------------------------+-------+
|  1 | 2014_10_12_000000_create_users_table                  |     1 |
|  2 | 2014_10_12_100000_create_password_resets_table        |     1 |
|  3 | 2019_08_19_000000_create_failed_jobs_table            |     1 |
|  4 | 2019_12_14_000001_create_personal_access_tokens_table |     1 |
|  5 | 2023_01_13_000001_add_new_column                      |     2 |
+----+-------------------------------------------------------+-------+

检查 users 表,可以看到 nickname 列已存在:

mysql> describe users;
+-------------------+-----------------+------+-----+---------+----------------+
| Field             | Type            | Null | Key | Default | Extra          |
+-------------------+-----------------+------+-----+---------+----------------+
| id                | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| name              | varchar(255)    | NO   |     | NULL    |                |
| email             | varchar(255)    | NO   | UNI | NULL    |                |
| email_verified_at | timestamp       | YES  |     | NULL    |                |
| password          | varchar(255)    | NO   |     | NULL    |                |
| remember_token    | varchar(100)    | YES  |     | NULL    |                |
| created_at        | timestamp       | YES  |     | NULL    |                |
| updated_at        | timestamp       | YES  |     | NULL    |                |
| nickname          | varchar(255)    | NO   |     | NULL    |                |
+-------------------+-----------------+------+-----+---------+----------------+

现在,如果由于某些原因需要撤销之前的迁移,可以运行以下命令以执行迁移脚本中的 down() 函数:

~ ❯ ./vendor/bin/sail artisan migrate:rollback --step=1

   INFO  Rolling back migrations.

  2023_01_13_000001_add_new_column ...................... 41ms DONE

查看表结构,可以看到 nickname 列已被移除。


此策略的优势

正如前面章节中提到的那样,版本化架构迁移的历史要比声明式方法悠久。这意味着开发者更可能熟悉其工作方式,并可能更舒适于这种环境。
许多支持版本化迁移的工具支持双向操作,即升级或降级架构。如果开发者或数据库管理员在迁移脚本中包含相关细节,单个脚本即可实现架构回退,这使得撤销变更更加简单。
最后,版本化迁移能够较容易地跟踪增量变化,即使不使用版本控制系统也是如此。由于所有迁移脚本被存储在一起,与声明式方法相比,诊断迁移问题可能更为直接。


此策略的缺陷

由于架构通过脚本逐步管理,在特定时间点获取数据库架构的完整视图可能会比较困难。通常需要在实际系统上回放所有之前的脚本才能看到完整架构。
视具体工具而定,它可能不会在尝试应用变更之前验证架构的当前状态。如果架构在工具之外被直接修改,例如直接对数据库执行 DDL,则会带来重大问题。


如何在 PlanetScale 上使用版本化架构迁移

是否启用了安全迁移(Safe Migrations),会决定你在 PlanetScale 中如何使用版本化迁移。

没有安全迁移

如果生产分支没有启用安全迁移,版本化迁移将像其他 MySQL 环境一样在 PlanetScale 分支中工作。理想情况下可以使用不同的数据库分支映射到不同的环境。代码准备好投入生产时,只需针对目标分支使用连接字符串运行相应迁移工具的升级命令即可完成迁移。
启用安全迁移是防止意外架构变更的最佳实践,同时还启用了其他有用功能。

启用了安全迁移

当生产分支启用了安全迁移时,会强制使用分支和部署请求来实现零停机迁移,同时限制直接使用 DDL。在这种情况下,应该创建一个开发分支,将开发环境连接到 PlanetScale 的开发分支,然后在开发分支中运行迁移。现在,开发分支将拥有已更新的架构,并准备好通过 PlanetScale 的部署请求合并到生产环境数据库。
通常,当通过部署请求合并数据库分支时,仅更改目标架构而不写入或修改任何数据。虽然这乍一看可能会是个问题(因为需要一个表来跟踪哪些更改已被应用),PlanetScale 提供了一种设置,可以在 Vitess 数据库之间自动复制迁移数据。这种设置可以选择几个预配置的 ORM,或者提供一个自定义的表名来同步数据库分支之间的数据。



版本化架构迁移插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:http://folen.top/2025/09/13/versioned-schema-migrations/