本节摘要:迁移把表结构变更写成带版本号的代码:每张表、每个字段的来龙去脉都有据可查、可回滚、可在任何一台机器上重放。本节讲迁移的生命周期、常用列类型与索引、改表的安全姿势,以及填充器与工厂怎么把空库变成有真实感数据的施工备料场。读完你能让全队在任何环境一键复原同一套数据库。
新手接项目常遇到这种对话:为什么我库里没有 orders 表?——群里发你一句"你手动跑一下那个 SQL"。表结构靠口头和散装 SQL 同步,是协作项目里最贵的坏习惯:环境不一致、字段没人说得清哪来的、改错了没法退。迁移(Migration)把地基施工变成代码:按时间排序的图纸,框架按序执行,migrations 表记录施工进度,支持整体回滚。
php artisan make:migration create_work_orders_table php artisan make:migration create_workers_table php artisan make:migration create_work_order_worker_table
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('work_orders', function (Blueprint $table) { $table->id(); // 大整数自增主键 $table->foreignId('team_id')->constrained() // 外键:队组 ->cascadeOnDelete(); $table->string('title', 100); // 标题 $table->string('sn', 32)->unique(); // 工单号 唯一索引 $table->unsignedInteger('budget')->default(0); // 预算 存分 $table->enum('status', ['draft', 'open', 'building', 'done']) ->default('draft'); $table->timestamp('due_at')->nullable(); $table->timestamps(); // created_at 与 updated_at }); } public function down(): void { Schema::dropIfExists('work_orders'); // 回滚就拆表 } };
写迁移有三条纪律。up 与 down 必须严格互逆,回滚是迁移的价值所在;改历史迁移文件是大忌——已经提交的图纸只能新增一份"变更迁移"去改,就像施工中的地基不能推翻重画;外键约束配 cascadeOnDelete 前先想清楚级联范围,删队组连带删光工单是否符合业务。
多对多中间表(工单与施工人员的关联)是第三个迁移:
<?php Schema::create('work_order_worker', function (Blueprint $table) { $table->id(); $table->foreignId('work_order_id')->constrained()->cascadeOnDelete(); $table->foreignId('worker_id')->constrained()->cascadeOnDelete(); $table->string('role', 30)->default('worker'); // 在这个单子上的工种 $table->timestamps(); $table->unique(['work_order_id', 'worker_id']); // 防重复派工 });
常用执行命令一把过:
php artisan migrate # 执行所有待跑迁移 php artisan migrate:rollback # 回滚最后一批 php artisan migrate:fresh # 全部拆掉重来(会清数据,慎用) php artisan migrate:status # 看施工进度

表建好了是空的,开发与测试都需要"看起来真实"的数据。工厂(Factory)定义每条记录怎么造,填充器(Seeder)决定造多少、怎么组合:
<?php // database/factories/WorkOrderFactory.php namespace Database\Factories; use Illuminate\Database\Eloquent\Factories\Factory; class WorkOrderFactory extends Factory { public function definition(): array { return [ 'team_id' => \App\Models\Team::factory(), 'title' => $this->faker->randomElement([ '外墙翻新', '电路改造', '防水补漏', '门窗更换', ]) . '工单', 'sn' => 'WO' . $this->faker->unique()->numerify('########'), 'budget' => $this->faker->numberBetween(10000, 500000), 'status' => $this->faker->randomElement(['draft', 'open', 'building', 'done']), 'due_at' => $this->faker->dateTimeBetween('now', '+3 months'), ]; } }
<?php // database/seeders/DatabaseSeeder.php public function run(): void { \App\Models\Team::factory()->count(3)->create(); \App\Models\WorkOrder::factory()->count(50)->create(); } // 命令行:php artisan db:seed
工厂造出来的数据在第九章测试里是主角:每条测试用例造自己的数据、跑完回滚,互不污染。本节先把工厂建好,到验收时直接取用。
💡 关键直觉:迁移加工厂,等于数据库结构与其中的样例数据都进了版本管理。新人入职跑两条命令(migrate 加 db:seed)就能得到一份和生产结构一致、带真实感数据的库——这比任何入职文档都管用。
地基打好了,接下来把施工队编制起来:下一节定义 Eloquent 模型,并用查询构造器写出复杂取数。