Laravel: Laravel数据库与迁移系统

最后更新:2026-08-26

迁移是 Laravel 的"数据库版本控制"——像 Git 管理代码一样管理数据库结构,团队协作不再怕表结构不同步。

1. 你将学到


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 核心表

100%
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+ 支持)。

📖 小节


📝 作业

  1. 基础题(⭐):为 ShopMetrics 创建 tenants、plans、subscriptions 三张表的迁移文件,包含合理的外键和索引,运行 migrate 确认无误。

  2. 进阶题(⭐⭐):在已有 orders 表基础上,创建迁移添加 discount 列(默认值 0)和复合索引(tenant_id + status),编写对应的 down() 方法确保可回滚。

  3. 挑战题(⭐⭐⭐):设计 ShopMetrics 的多态关联迁移——让 shops 和 products 都可以有地址(addresses 表用 morphTo),实现 morphable_type + morphable_id 的外键设计。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏