Laravel 13 数据库迁移进阶

数据库迁移是 Laravel 数据库版本控制的核心功能,提供了优雅的数据库结构管理方式。本文将深入探讨 Laravel 13 数据库迁移的高级用法。

创建迁移

基本迁移

1
2
3
php artisan make:migration create_users_table
php artisan make:migration add_role_to_users_table --table=users
php artisan make:migration create_orders_table --create=orders

迁移结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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('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();
$table->softDeletes();
});
}

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

表结构操作

创建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->text('excerpt')->nullable();
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
$table->integer('views')->default(0);
$table->decimal('price', 10, 2)->default(0);
$table->boolean('is_featured')->default(false);
$table->json('metadata')->nullable();
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->softDeletes();

$table->index(['status', 'published_at']);
$table->fullText(['title', 'content']);
});

修改表

1
2
3
4
5
6
7
Schema::table('users', function (Blueprint $table) {
$table->string('name', 100)->change();
$table->string('phone')->after('email');
$table->string('role')->default('user')->after('password');
$table->renameColumn('name', 'full_name');
$table->dropColumn('old_column');
});

重命名表

1
Schema::rename('old_table', 'new_table');

删除表

1
2
Schema::dropIfExists('users');
Schema::drop('users');

列类型

常用列类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
$table->id();
$table->bigIncrements('id');
$table->uuid('id')->primary();
$table->ulid('id')->primary();

$table->string('name', 100);
$table->text('description');
$table->longText('content');
$table->mediumText('summary');

$table->integer('votes');
$table->bigInteger('views');
$table->smallInteger('order');
$table->tinyInteger('status');

$table->float('amount', 8, 2);
$table->double('price', 10, 2);
$table->decimal('total', 12, 2);

$table->boolean('active');
$table->enum('status', ['pending', 'approved', 'rejected']);
$table->set('tags', ['php', 'laravel', 'mysql']);

$table->date('birth_date');
$table->dateTime('published_at');
$table->timestamp('created_at');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->useCurrentOnUpdate();

$table->year('year');
$table->time('start_time');

$table->json('settings');
$table->jsonb('data');
$table->binary('file');

$table->ipAddress('ip');
$table->macAddress('mac');
$table->morphs('taggable');
$table->nullableMorphs('taggable');
$table->rememberToken();
$table->softDeletes();
$table->softDeletesTz();

索引

创建索引

1
2
3
4
5
6
7
8
9
$table->primary('id');
$table->primary(['id', 'parent_id']);
$table->unique('email');
$table->unique(['email', 'username']);
$table->index('name');
$table->index(['name', 'email']);
$table->fullText('content');
$table->fullText(['title', 'content']);
$table->spatialIndex('location');

命名索引

1
2
$table->unique('email', 'users_email_unique');
$table->index('name', 'users_name_index');

删除索引

1
2
3
4
5
$table->dropPrimary('id');
$table->dropUnique('users_email_unique');
$table->dropIndex('users_name_index');
$table->dropFullText('posts_content_fulltext');
$table->dropSpatialIndex('locations_location_spatialindex');

外键约束

创建外键

1
2
3
4
5
6
7
8
9
10
11
12
13
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->foreignId('category_id')->constrained('categories')->cascadeOnDelete();
$table->foreignId('author_id')->nullable()->constrained('users')->nullOnDelete();
$table->foreignUuid('team_id')->constrained();
});

// 或手动创建
Schema::table('posts', function (Blueprint $table) {
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});

外键操作

1
2
3
4
5
6
7
8
9
$table->dropForeign('posts_user_id_foreign');
$table->dropForeign(['user_id']);

$table->cascadeOnDelete();
$table->cascadeOnUpdate();
$table->restrictOnDelete();
$table->restrictOnUpdate();
$table->nullOnDelete();
$table->noActionOnDelete();

数据库事务

迁移中的事务

1
2
3
4
5
6
7
8
9
10
use Illuminate\Support\Facades\DB;

public function up(): void
{
DB::transaction(function () {
DB::table('users')->insert([
['name' => 'Admin', 'email' => 'admin@example.com'],
]);
});
}

迁移命令

运行迁移

1
2
3
4
php artisan migrate
php artisan migrate --force
php artisan migrate --path=/path/to/migration.php
php artisan migrate --step

回滚迁移

1
2
3
php artisan migrate:rollback
php artisan migrate:rollback --step=5
php artisan migrate:rollback --path=/path/to/migration.php

重置迁移

1
2
3
4
5
php artisan migrate:reset
php artisan migrate:fresh
php artisan migrate:fresh --seed
php artisan migrate:refresh
php artisan migrate:refresh --seed

查看状态

1
php artisan migrate:status

高级迁移技巧

条件迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();

if (config('database.default') === 'mysql') {
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
}

$table->string('title');
});
}

检查表/列是否存在

1
2
3
4
5
6
7
8
if (Schema::hasTable('users')) {
}

if (Schema::hasColumn('users', 'email')) {
}

if (Schema::hasColumns('users', ['email', 'name'])) {
}

获取列类型

1
$type = Schema::getColumnType('users', 'email');

数据库连接

1
2
3
Schema::connection('secondary')->create('users', function (Blueprint $table) {
$table->id();
});

迁移模板

完整迁移示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<?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('orders', function (Blueprint $table) {
$table->id();
$table->uuid('order_number')->unique();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();

$table->enum('status', [
'pending',
'processing',
'shipped',
'delivered',
'cancelled',
'refunded'
])->default('pending');

$table->decimal('subtotal', 12, 2)->default(0);
$table->decimal('tax', 12, 2)->default(0);
$table->decimal('shipping', 12, 2)->default(0);
$table->decimal('discount', 12, 2)->default(0);
$table->decimal('total', 12, 2)->default(0);

$table->string('currency', 3)->default('USD');

$table->json('shipping_address');
$table->json('billing_address');
$table->json('metadata')->nullable();

$table->text('notes')->nullable();

$table->timestamp('ordered_at')->useCurrent();
$table->timestamp('processed_at')->nullable();
$table->timestamp('shipped_at')->nullable();
$table->timestamp('delivered_at')->nullable();
$table->timestamp('cancelled_at')->nullable();

$table->timestamps();
$table->softDeletes();

$table->index(['user_id', 'status']);
$table->index('created_at');
$table->index('ordered_at');
});

Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')->constrained()->cascadeOnDelete();
$table->foreignId('product_id')->constrained()->restrictOnDelete();

$table->string('product_name');
$table->string('product_sku');
$table->integer('quantity')->default(1);
$table->decimal('unit_price', 12, 2);
$table->decimal('total_price', 12, 2);

$table->json('options')->nullable();

$table->timestamps();

$table->index(['order_id', 'product_id']);
});
}

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

最佳实践

1. 使用描述性名称

1
2
3
php artisan make:migration create_orders_table
php artisan make:migration add_status_to_orders_table
php artisan make:migration remove_deprecated_column_from_users_table

2. 保持迁移可逆

1
2
3
4
5
6
7
8
9
10
11
12
13
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable();
});
}

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

3. 使用外键约束

1
2
3
4
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete()
->cascadeOnUpdate();

4. 添加适当的索引

1
2
$table->index(['status', 'created_at']);
$table->unique(['user_id', 'product_id']);

总结

Laravel 13 的数据库迁移系统提供了强大而灵活的数据库版本控制能力。通过合理使用迁移,可以轻松管理数据库结构变更,确保团队协作时数据库结构的一致性。记住使用描述性的迁移名称、保持迁移可逆、合理使用索引和外键约束,并在生产环境中小心执行破坏性操作。迁移是 Laravel 数据库开发的核心工具,掌握它对于构建可维护的应用程序至关重要。