Laravel: Laravel数据库与迁移系统
最后更新:2026-08-26
迁移是 Laravel 的"数据库版本控制"——像 Git 管理代码一样管理数据库结构,团队协作不再怕表结构不同步。
1. 你将学到
- 迁移文件创建与执行:make:migration / migrate / rollback
- Schema Builder:列类型、索引、外键约束
- 多租户表设计:tenants/users/subscriptions/plans/orders
- 迁移策略:生产环境安全迁移与零停机
- MySQL 与 PostgreSQL 迁移差异处理
2. 一个 DBA 的真实故事
(1) 痛点:手动执行 SQL 导致生产事故
Charlie 在生产环境手动执行了一条 ALTER TABLE orders ADD COLUMN discount DECIMAL(8,2),但忘了加默认值——200 万条记录的 discount 列全部为 NULL,导致报表模块崩溃 2 小时。更糟的是,Bob 在本地加了一个字段但没告诉 Charlie,代码推上去后直接 SQL 报错。
(2) Laravel 迁移的解法
Laravel 迁移用 PHP 代码描述数据库变更,团队共享迁移文件,执行顺序自动追踪——每个人 php artisan migrate 后数据库结构完全一致。
BASH
php artisan make:migration add_discount_to_orders_table
# Creates a timestamped migration file
php artisan migrate
# Runs all pending migrations in order
(3) 收益
Charlie 用迁移替代手动 SQL 后,新增字段必须指定默认值(代码审查就能发现),Bob 的本地变更推到 Git 后 Charlie 一条命令同步,0 事故。
3. 迁移基础
(1) 创建迁移文件
BASH
# Create a migration
php artisan make:migration create_shops_table
# With table name hint
php artisan make:migration create_shops_table --create=shops
# Add columns to existing table
php artisan make:migration add_status_to_shops_table --table=shops
(2) 迁移文件结构
PHP
// database/migrations/2024_01_15_000000_create_shops_table.php
return new class extends Migration
{
public function up(): void
{
Schema::create('shops', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->enum('status', ['active', 'suspended', 'closed'])->default('active');
$table->decimal('revenue', 12, 2)->default(0);
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('shops');
}
};
(3) 迁移命令
| 命令 | 作用 |
|---|---|
migrate |
执行未运行的迁移 |
migrate:rollback |
回滚上一批迁移 |
migrate:refresh |
回滚所有 + 重新执行 |
migrate:fresh |
删库 + 重新执行 |
migrate:status |
查看迁移状态 |
migrate:reset |
回滚所有迁移 |
▶ 示例:创建和执行迁移
BASH
# Create migration
php artisan make:migration create_shops_table
# Run pending migrations
php artisan migrate
# 2024_01_15_000000_create_shops_table .............. done
# Check migration status
php artisan migrate:status
# Ran? Migration
# Yes 0001_01_01_000000_create_users_table
# Yes 2024_01_15_000000_create_shops_table
# No 2024_01_16_000000_create_products_table
输出:
TEXT
📖 仅展示
# 命令执行成功
4. Schema Builder
(1) 常用列类型
| 方法 | 数据库类型 | 说明 |
|---|---|---|
id() |
BIGINT UNSIGNED AUTO_INCREMENT | 主键 |
foreignId('x') |
BIGINT UNSIGNED | 外键 |
string('name', 255) |
VARCHAR | 字符串 |
text('content') |
TEXT | 长文本 |
integer('count') |
INT | 整数 |
decimal('price', 8, 2) |
DECIMAL(8,2) | 精确小数 |
boolean('active') |
TINYINT(1) | 布尔 |
enum('status', [...]) |
ENUM | 枚举 |
json('metadata') |
JSON | JSON 数据 |
timestamp('published_at') |
TIMESTAMP | 时间戳 |
softDeletes() |
TIMESTAMP NULL | 软删除 |
(2) 索引与约束
PHP
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('shop_id')->constrained()->cascadeOnDelete();
$table->string('order_number')->unique();
$table->decimal('total', 12, 2);
// Indexes
$table->index('shop_id'); // Single index
$table->index(['shop_id', 'status']); // Composite index
$table->unique(['tenant_id', 'order_number']); // Unique composite
// Foreign key constraints
$table->foreignId('user_id')
->constrained('users') // Custom table name
->cascadeOnDelete() // Delete related on parent delete
->cascadeOnUpdate(); // Update related on parent update
$table->timestamps();
});
| 约束方法 | 作用 |
|---|---|
unique() |
唯一索引 |
index() |
普通索引 |
constrained() |
自动推断外键关系 |
cascadeOnDelete() |
级联删除 |
restrictOnDelete() |
限制删除(有子记录则报错) |
nullOnDelete() |
删除父记录时设 NULL |
▶ 示例:ShopMetrics 多租户外键设计
PHP
// database/migrations/create_tenants_table.php
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('domain')->unique();
$table->enum('status', ['active', 'suspended', 'cancelled'])->default('active');
$table->timestamps();
$table->softDeletes();
});
// database/migrations/create_subscriptions_table.php
Schema::create('subscriptions', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('plan_id')->constrained()->cascadeOnDelete();
$table->enum('status', ['active', 'past_due', 'cancelled'])->default('active');
$table->timestamp('trial_ends_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'status']);
});
输出:
TEXT
📖 仅展示
// 执行成功
5. 多租户表设计
(1) ShopMetrics 核心表
timeline
title ShopMetrics Migration Timeline
Create tenants : tenants table
Create plans : plans table
Create users : users table with tenant_id
Create subscriptions : subscriptions table
Create shops : shops table with tenant_id
Create products : products table with shop_id
Create categories : categories table
Create category_product : pivot table
Create orders : orders table with tenant_id + shop_id
Create order_items : order_items table with order_id + product_id
(2) 完整迁移顺序
| 顺序 | 表名 | 核心字段 |
|---|---|---|
| 1 | tenants | id, name, slug, domain, status |
| 2 | plans | id, name, price, features(JSON) |
| 3 | users | id, tenant_id(FK), name, email, role |
| 4 | subscriptions | id, tenant_id(FK), plan_id(FK), status |
| 5 | shops | id, tenant_id(FK), name, slug, revenue |
| 6 | products | id, shop_id(FK), name, price, sku |
| 7 | categories | id, name, slug |
| 8 | category_product | category_id(FK), product_id(FK) |
| 9 | orders | id, tenant_id(FK), shop_id(FK), total, status |
| 10 | order_items | id, order_id(FK), product_id(FK), qty, price |
▶ 示例:ShopMetrics 完整用户表迁移
PHP
// database/migrations/create_users_table.php
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->enum('role', ['super_admin', 'tenant_owner', 'analyst', 'viewer'])
->default('viewer');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->index(['tenant_id', 'role']);
$table->index('email');
});
输出:
TEXT
📖 仅展示
// 执行成功
6. 生产迁移策略
(1) 安全迁移原则
| 原则 | 说明 | 违反后果 |
|---|---|---|
| 新列必须有默认值 | ->default(0) 或 ->nullable() |
已有记录报错 |
| 不要删列先删代码 | 先停止使用该列,再下个版本删 | 代码引用不存在的列 |
大表加列用 after() |
指定列位置减少表重建 | 锁表时间过长 |
| 迁移包在事务内 | withinTransaction 属性 |
部分成功部分失败 |
(2) MySQL vs PostgreSQL 差异
| 特性 | MySQL | PostgreSQL |
|---|---|---|
| 加列默认值 | 瞬间(INSTANT ADD) | 需重写表 |
| JSON 列 | json() |
json() + jsonb() |
| 枚举 | enum() |
建议 string + CHECK |
| 全文索引 | fullText() |
fullText() + GIN |
| 外键检查 | 可临时关闭 | 严格检查 |
▶ 示例:安全添加列到生产大表
PHP
// Safe: Add column with default value
Schema::table('orders', function (Blueprint $table) {
$table->decimal('discount', 8, 2)
->default(0)
->after('total');
});
// Safe: Make column nullable first
Schema::table('shops', function (Blueprint $table) {
$table->string('phone')->nullable()->after('email');
});
// DANGEROUS: Removing column — do in two steps
// Step 1: This release — stop using the column in code
// Schema::table('shops', function (Blueprint $table) {
// $table->dropColumn('legacy_field');
// });
输出:
TEXT
📖 仅展示
// 执行成功
7. 修改表结构
(1) 修改列
BASH
composer require doctrine/dbal
# Required for modifying existing columns
PHP
Schema::table('shops', function (Blueprint $table) {
$table->string('name', 100)->change(); // Change length
$table->renameColumn('desc', 'description'); // Rename column
$table->dropColumn('legacy_field'); // Drop column
});
(2) 修改索引
PHP
Schema::table('orders', function (Blueprint $table) {
$table->dropUnique('orders_order_number_unique');
$table->unique(['tenant_id', 'order_number'], 'orders_tenant_order_unique');
});
▶ 示例:ShopMetrics 迁移修改实战
PHP
// database/migrations/2024_02_01_add_stripe_to_subscriptions.php
return new class extends Migration
{
public function up(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->string('stripe_id')->nullable()->unique()->after('id');
$table->string('stripe_status')->nullable()->after('status');
$table->timestamp('current_period_start')->nullable()->after('trial_ends_at');
$table->timestamp('current_period_end')->nullable()->after('current_period_start');
});
}
public function down(): void
{
Schema::table('subscriptions', function (Blueprint $table) {
$table->dropColumn([
'stripe_id', 'stripe_status',
'current_period_start', 'current_period_end',
]);
});
}
};
输出:
TEXT
📖 仅展示
// 执行成功
8. 综合示例:ShopMetrics 完整迁移集
PHP
// ============================================
// Comprehensive: ShopMetrics Core Migrations
// Covers: tables, foreign keys, indexes, polymorphic
// ============================================
// Migration 1: Create plans table
Schema::create('plans', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->decimal('price', 8, 2);
$table->integer('shop_limit')->default(5);
$table->integer('order_limit')->default(1000);
$table->json('features')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
// Migration 2: Create tenants table
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('domain')->unique();
$table->foreignId('plan_id')->nullable()->constrained()->nullOnDelete();
$table->enum('status', ['active', 'suspended', 'cancelled'])->default('active');
$table->timestamps();
$table->softDeletes();
$table->index(['status', 'created_at']);
});
// Migration 3: Create shops table
Schema::create('shops', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('slug');
$table->text('description')->nullable();
$table->decimal('revenue', 12, 2)->default(0);
$table->enum('status', ['active', 'suspended', 'closed'])->default('active');
$table->timestamps();
$table->softDeletes();
$table->unique(['tenant_id', 'slug']);
$table->index(['tenant_id', 'status']);
});
// Migration 4: Create products table
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('shop_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->string('sku')->unique();
$table->decimal('price', 10, 2);
$table->integer('stock')->default(0);
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
$table->index(['shop_id', 'is_active']);
});
// Migration 5: Create orders table
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->foreignId('shop_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('order_number')->unique();
$table->decimal('subtotal', 12, 2);
$table->decimal('discount', 8, 2)->default(0);
$table->decimal('total', 12, 2);
$table->enum('status', ['pending', 'processing', 'completed', 'cancelled'])->default('pending');
$table->json('metadata')->nullable();
$table->timestamps();
$table->index(['tenant_id', 'status']);
$table->index(['shop_id', 'created_at']);
});
❓ 常见问题
Q migrate:fresh 和 migrate:refresh 有什么区别?
A fresh 先删库再重建,速度更快但数据全部丢失;refresh 先 rollback 再 migrate,会依次执行 down() 再 up()。开发环境用 fresh 更快,生产环境绝不能用这两个命令。
Q 外键约束会影响性能吗?
A 会。每次 INSERT/UPDATE/DELETE 都需要检查外键约束。高并发场景可以考虑不加外键约束,改在应用层保证数据一致性。
Q 生产环境怎么安全执行迁移?
A 先在 staging 环境测试迁移;加列必须有默认值或 nullable;删列分两步(先删代码引用,下版本删列);大表迁移在低峰期执行。
Q 迁移文件可以修改吗?
A 未执行(未 migrate)的迁移可以随意修改;已执行的迁移不要修改,应该新建迁移来做变更。否则其他环境的 migrate 会报错。
Q SQLite 和 MySQL 的迁移有什么兼容问题?
A SQLite 不支持 ALTER TABLE 的某些操作(如修改列类型、删列),开发时用 SQLite 可能在迁移时遇到限制。生产环境建议 MySQL/PostgreSQL。
Q 如何给已有大表加索引而不锁表?
A MySQL 使用
ALGORITHM=INPLACE LOCK=NONE(Laravel 不直接支持,需用 DB::statement);PostgreSQL 使用 CONCURRENTLY($table->index('col')->concurrently() 在 Laravel 11+ 支持)。📖 小节
- 迁移是数据库的版本控制,团队共享迁移文件确保结构一致
- Schema Builder 用 PHP 代码描述表结构,支持列类型、索引、外键
- 外键约束保证引用完整性,但高并发场景可考虑应用层保证
- 多租户表设计通过 tenant_id 外键实现数据隔离
- 生产迁移必须安全:新列加默认值、删列分两步
- migrate:fresh 开发用,生产只用 migrate
📝 作业
-
基础题(⭐):为 ShopMetrics 创建 tenants、plans、subscriptions 三张表的迁移文件,包含合理的外键和索引,运行 migrate 确认无误。
-
进阶题(⭐⭐):在已有 orders 表基础上,创建迁移添加 discount 列(默认值 0)和复合索引(tenant_id + status),编写对应的 down() 方法确保可回滚。
-
挑战题(⭐⭐⭐):设计 ShopMetrics 的多态关联迁移——让 shops 和 products 都可以有地址(addresses 表用 morphTo),实现 morphable_type + morphable_id 的外键设计。