首页
/ Coolify 中 383 个 Laravel 迁移文件的治理规范:以大规模迁移体系印证七条数据库迁移最佳实践

Coolify 中 383 个 Laravel 迁移文件的治理规范:以大规模迁移体系印证七条数据库迁移最佳实践

2026-09-04 21:53:50作者:魏侃纯Zoe

本篇以 Laravel 迁移最佳实践规则为骨架,结合 Coolify(一个可自托管、对标 Vercel/Heroku/Netlify 的开源 PaaS)仓库中 383 个真实迁移文件的写法,逐条展开「Artisan 生成迁移、外键 constrained、不可修改已部署迁移、建表即建索引、模型镜像默认值、可回滚 down() 方法、单一职责迁移」七项规范的具体含义与落地方式。读完本文,你将掌握在大型 Laravel 项目中编写、演进和回滚数据库迁移的完整方法论,并能在 Coolify 迁移目录 中找到每条规则的源码级实证。

Coolify 的迁移体系:规范从何而来

Coolify 是典型的「长周期演进」Laravel 项目:从 database/migrations 目录看,当前仓库累计沉淀了 383 个迁移文件,时间跨度从 2014 年的框架基础表(用户、密码重置、会话)一直延续到 2025 年的云服务商令牌等新增表。这种量级意味着迁移本身已经成为项目最重要的「数据架构文档」——任何一个在 CI 或生产环境中被执行过的迁移,都牵动着所有已部署实例的数据库状态。

因此,Coolify 将迁移实践规范沉淀为独立规则文档 migrations.md,作为团队协作与代码审查的依据。下文按该文档的七条规则逐一展开,并用仓库中的真实迁移文件佐证每条规则的执行情况。

一、统一用 Artisan 生成迁移:命名即契约

规则文档的第一条要求:永远使用 php artisan make:migration 生成迁移文件,以获得一致的命名与时间戳,禁止手工创建无时间戳的文件。

# 正确做法(Artisan 生成)
php artisan make:migration create_posts_table
php artisan make:migration add_slug_to_posts_table

对比手工创建的 posts_migration.php(无时间戳、命名随意),Artisan 生成的文件名遵循 Y_m_d_His_action_description.php 格式。时间戳前缀直接决定了 php artisan migrate 的执行顺序,是迁移幂等性与可重放性的基础。

Coolify 仓库的命名风格可以完整印证这一点,例如:

动词前缀 create_*add_* 让文件按时间排序后,表结构演进脉络一目了然——这正是时间戳命名带来的「可审计性」。

二、外键统一使用 constrained():自动命名 + 引用完整性

规则要求所有外键列使用 foreignId(...)->constrained(),让 Laravel 自动推导约束名称({表}_外键列_foreign)并真正创建数据库级外键;非标准表名时可显式指定:

$table->foreignId('user_id')->constrained()->cascadeOnDelete();

// 外键指向非约定命名的表
$table->foreignId('author_id')->constrained('users');

Coolify 的迁移中 constrained() 几乎是外键列的标准写法,且清晰展示了三种级联策略的选择:

  1. cascadeOnDelete() / onDelete('cascade')——子记录随父记录删除。通知配置类表全部采用此策略,如 team_invitations 迁移 中的 $table->foreignId('team_id')->constrained()->cascadeOnDelete();,以及 Slack 通知配置迁移邮件DiscordTelegramPushover 等同类表。团队删除后其通知配置随之级联清理,避免孤儿配置行;
  2. onDelete('set null')——父记录删除时置空外键,用于「可选关联」。例如 服务器表追加 cloud_provider_token_id 列的迁移$table->foreignId('cloud_provider_token_id')->nullable()->after('private_key_id')->constrained()->onDelete('set null');。云令牌被删除时,服务器记录保留、仅解除关联,这是典型的「软解除引用」场景;
  3. 可空外键 + 可空级联——共享环境变量迁移 展示了变量可绑定到 team/project/environment 中任意一层的灵活建模:$table->foreignId('project_id')->nullable()->constrained()->onDelete('cascade');

值得注意的是,并非所有 foreignId 都带 constrained()。例如 applications 建表迁移$table->foreignId('environment_id'); 未加约束——从源码结构看,这是早期迁移的遗留写法(可能为规避迁移顺序或大表加外键的锁表开销)。对新迁移而言,规则文档的立场是明确的:能用 constrained() 就应当用,把引用完整性交给数据库而非应用层校验。

三、已部署的迁移视为不可变:只新增、不修改

这是迁移规范中最关键的一条:一旦迁移在生产环境执行过,就把它当作只读文件;要改表,就新建一个迁移

文档中的反例是编辑已上线的 2024_01_01_create_posts_table.php 追加 slug 列;正确做法是新增 add_slug_to_posts_table 迁移,在 down()dropColumn 实现回滚。

这条规则在 Coolify 的演进史中被反复验证:applications 表自 2023 年 3 月创建后,从未被「就地修改」,其后续所有字段变更都以新迁移追加,例如 2023_08_06_142951_add_description_field_to_applications_table2024_05_15_091757_add_commit_message_to_app_deployment_queue 等一系列 add_* 文件。383 个迁移文件本质上就是一份「只追加(append-only)」的表结构变更日志——如果允许编辑旧文件,任何新实例执行 migrate 得到的 schema 都会与存量实例分叉,而「不可变 + 新迁移」保证了任意时点全新拉起的数据库与存量数据库的终态一致。

四、建表时就把索引建好:WHERE / ORDER BY / JOIN 的列

规则要求:凡用于 WHEREORDER BYJOIN 的列,应在 Schema::create 阶段直接加索引,而不是事后补迁移。文档给出的标准示例:

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->index();
    $table->string('status')->index();
    $table->timestamp('shipped_at')->nullable()->index();
    $table->timestamps();
});

Coolify 仓库中同类写法同样存在,如 会话表迁移$table->foreignId('user_id')->nullable()->index();$table->integer('last_activity')->index();——会话查询几乎总是按 user_id 过滤、按 last_activity 排序,索引在表结构诞生之初就与列同时落地,省去了后续「加索引 = 大表锁/重建」的成本。此外 unique() 约束(如 team_invitations 中的 $table->unique(['team_id', 'email']);)在多数数据库中也由索引支撑,属于同一类「建表即完成」的决策。

五、数据库默认值与模型 $attributes 双写镜像

当列定义了数据库默认值时,规则要求在模型的 $attributes 中镜像同样的默认值,使未持久化的新实例在保存前就持有正确值:

// Migration
$table->string('status')->default('pending');

// Model
protected $attributes = [
    'status' => 'pending',
];

Coolify 模型中这一模式真实存在:Team 模型 声明了 protected $attributes = ['is_mcp_server_enabled' => true];,其默认值与建表迁移中的列默认值语义一致——这样 new Team 后未显式赋值时,序列化、API 响应、表单回填都拿到与数据库一致的初始值,而不是 null。仓库中 EnvironmentVariableInstanceSettingsServiceApplication 等模型也采用了相同的 $attributes 镜像写法。

对照 applications 建表迁移 可以看到大量 ->default(...) 列(health_check_method 默认 GETgit_commit_sha 默认 HEAD 等)。这些默认值对「数据库侧直接插入」是兜底,而模型侧的镜像默认值则覆盖了「应用侧构造对象再落库」的路径,两处默认值保持一致是这条规则成立的前提。

六、默认编写可回滚的 down():让 migrate:rollback 在 CI 与事故现场可用

规则要求:凡是能安全逆操作的 schema 变更,都应实现 down(),使 php artisan migrate:rollback 可用于 CI 环境重置与部署失败回退。

Coolify 的建表迁移普遍遵循此约定,例如 projects 建表迁移

public function up(): void
{
    Schema::create('projects', function (Blueprint $table) {
        $table->id();
        $table->string('uuid')->unique();
        $table->string('name');
        $table->string('description')->nullable();
        $table->foreignId('team_id');
        $table->timestamps();
    });
}

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

Coolify 的测试体系依赖可重放的迁移链:phpunit.xml 强制 DB_CONNECTION=testing,配合 config/testing.php 的测试库配置,功能测试(tests/Feature 下 500+ 个测试文件)需要在隔离数据库中干净地重跑整套迁移。down() 方法的可执行性,直接决定了 migrate:fresh / rollback 能否在 CI 中稳定工作。

规则同时给出一条务实的边界:对于故意不可逆的迁移(如破坏性数据回填),不要假装支持回滚——留一条清晰的注释说明不可逆原因,并以「向前修复(forward fix)」新迁移代替回滚。这与「不可变迁移」原则一致:回滚能力是设计出来的,不是事后补救出来的。

七、一个迁移只解决一件事:DDL 与 DML 分离

规则的最后一条:每个迁移聚焦单一关注点,绝不混用 DDL(改结构)和 DML(动数据)。文档给出的反例是:在 up() 里既 Schema::create('settings', ...)DB::table('settings')->insert([...])——一旦前半成功、后半失败,数据库将停留在「表已建、种子数据缺失」的不可恢复中间态;正确做法是拆成 create_settings_tableseed_default_settings 两个迁移。

这里值得对照 Coolify 仓库中的真实案例做一点深入讨论。Slack 通知配置迁移up() 确实同时包含了 DDL 与 DML:建表之后遍历所有存量团队,为每个 team 插入一行默认通知配置,并用 try/catch + Log::error 包裹单条插入以保证批量循环的幂等与容错:

foreach ($teams as $team) {
    try {
        DB::table('slack_notification_settings')->insert(['team_id' => $team->id]);
    } catch (\Throwable $e) {
        Log::error('Error creating slack notification settings for existing teams: '.$e->getMessage());
    }
}

从源码结构看,这是对「存量数据升级」场景的务实妥协:新表带 team_id 唯一约束,每个团队必须立刻拥有一行配置(后续 Team 模型的 booted 钩子 已改为在新建团队时自动创建各行通知配置)。严格遵循「单一职责」时应当拆分为建表迁移 + 数据回填迁移两个文件;而把两者合并在同一文件内、辅以逐条容错,则换来了「建表与回填原子生效」的运维便利。理解这一取舍,比机械背诵规则更有价值:规范定义的是默认姿势,而存量数据升级是需要显式权衡的例外场景。

实践清单:提交迁移前逐项核对

综合以上七条规则,可以在代码审查中使用如下检查清单:

检查项 依据
文件由 php artisan make:migration 生成,文件名含时间戳与 create_/add_ 语义前缀 命名规范;参见 tags 建表迁移
所有外键列使用 foreignId(...)->constrained(),并显式选择 cascadeOnDelete / set null 等策略 team_invitationscloud_provider_token 列
未修改任何已部署迁移;表结构变更一律新增 add_* 迁移 Coolify 迁移目录 的 append-only 演进模式
WHERE/ORDER BY/JOIN 列在 Schema::create 阶段即加 index()/unique() sessions 表
数据库列默认值在模型 protected $attributes 中镜像 Team 模型applications 建表迁移
down() 实现可回滚;确不可逆处留注释并改用向前修复 projects 建表迁移
一个迁移只做一件事;DDL 与 DML 分离(存量回填例外需显式说明并容错) Slack 配置迁移案例

对自托管 PaaS 这类「用户各自持有数据库」的项目,迁移的可重放性就是产品承诺的一部分:新安装、旧库升级、CI 重置三条路径最终必须收敛到同一份 schema。以上七条规则覆盖了从命名、外键、索引到回滚的完整生命周期,而 Coolify 仓库中 383 个迁移文件则提供了每一条规则在大体量、长周期项目中的真实落点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384