Lzh on GitHub

介绍

迁移(Migrations)就像数据库的版本控制,允许你的团队定义并共享应用的数据库结构。如果你曾经在从版本控制拉取更新后,还需要告诉同事手动在本地数据库中添加某个字段,那么你就遇到了迁移所解决的问题。

Laravel 的 Schema 门面(Schema facade)提供了与数据库无关的支持,可以在 Laravel 所支持的所有数据库系统中创建和操作数据表。通常,迁移会使用这个门面来创建和修改数据库表及字段。

生成迁移

你可以使用 Artisan 命令 make:migration 来生成数据库迁移文件。新迁移文件会被放置在 database/migrations 目录下。每个迁移文件名都包含一个时间戳,Laravel 会根据时间戳来确定迁移的执行顺序:

php artisan make:migration create_flights_table

Laravel 会根据迁移名称尝试推断表名,以及该迁移是否用于创建新表。如果 Laravel 能够从迁移名称中确定表名,它会在生成的迁移文件中预先填充指定的表名。否则,你可以在迁移文件中手动指定表名。

如果你希望为生成的迁移文件指定自定义路径,可以在执行 make:migration 命令时使用 --path 选项。指定的路径应相对于应用的根路径。

此外,迁移模板(stubs)也可以通过发布模板进行自定义。

合并迁移

随着应用的不断开发,你可能会积累越来越多的迁移文件,这会导致 database/migrations 目录膨胀,甚至可能有上百个迁移文件。如果需要,你可以将这些迁移“合并”为一个单独的 SQL 文件。首先,可以执行 schema:dump 命令:

php artisan schema:dump

# 导出当前数据库结构并清理已有的迁移文件...
php artisan schema:dump --prune

执行该命令后,Laravel 会在应用的 database/schema 目录下生成一个“schema”文件。该文件的名称会对应数据库连接名称。现在,当你尝试迁移数据库且没有其他迁移被执行时,Laravel 会先执行所使用数据库连接的 schema 文件中的 SQL 语句。执行完 schema 文件的 SQL 后,Laravel 会继续执行任何未包含在 schema dump 中的迁移文件。

如果你的应用测试使用的数据库连接与本地开发时使用的连接不同,你应确保已使用该测试连接导出 schema 文件,以便测试可以正常构建数据库。通常可以在导出本地开发数据库连接的 schema 后执行:

php artisan schema:dump
php artisan schema:dump --database=testing --prune

建议将数据库 schema 文件提交到版本控制,这样团队中新加入的开发者可以快速创建应用的初始数据库结构。

迁移合并功能仅适用于 MariaDB、MySQL、PostgreSQL 和 SQLite 数据库,并依赖数据库的命令行客户端。

迁移结构

迁移类包含两个方法:updownup 方法用于向数据库中添加新的表、字段或索引,而 down 方法则应当撤销 up 方法执行的操作。

在这两个方法中,你可以使用 Laravel 的 Schema 构建器(Schema builder)来直观地创建和修改数据表。关于 Schema 构建器的所有方法,请参考其官方文档。例如,以下迁移创建了一个 flights 表:

<?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('flights', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('airline');
            $table->timestamps();
        });
    }

    /**
     * 回滚迁移
     */
    public function down(): void
    {
        Schema::drop('flights');
    }
};

设置迁移连接

如果你的迁移需要操作非应用默认的数据库连接,应当在迁移类中设置 $connection 属性:

/**
 * 迁移使用的数据库连接
 *
 * @var string
 */
protected $connection = 'pgsql';

/**
 * 执行迁移
 */
public function up(): void
{
    // ...
}

跳过迁移

有时某个迁移用于支持尚未启用的功能,你可能暂时不希望它被执行。这时,可以在迁移类中定义 shouldRun 方法。如果该方法返回 false,迁移将被跳过:

use App\Models\Flights;
use Laravel\Pennant\Feature;

/**
 * 判断该迁移是否应当执行
 */
public function shouldRun(): bool
{
    return Feature::active(Flights::class);
}

执行迁移

要执行所有尚未运行的迁移,可以使用 Artisan 命令 migrate

php artisan migrate

如果你想查看哪些迁移已经执行,哪些仍未执行,可以使用 migrate:status 命令:

php artisan migrate:status

如果你希望查看迁移将要执行的 SQL 语句,但不实际运行迁移,可以在 migrate 命令中使用 --pretend 参数:

php artisan migrate --pretend

隔离迁移执行

在多服务器部署应用时,如果在部署过程中执行迁移,你通常不希望两个服务器同时尝试迁移数据库。为避免这种情况,可以在执行 migrate 命令时使用 --isolated 选项。

当提供 --isolated 选项时,Laravel 会在尝试运行迁移前使用应用的缓存驱动获取原子锁(atomic lock)。在锁被持有期间,其他所有迁移命令尝试将不会执行,但命令仍会以成功状态退出:

php artisan migrate --isolated
要使用此功能,应用必须使用 memcached、redis、dynamodb、database、file 或 array 作为默认缓存驱动,并且所有服务器必须连接到同一个中央缓存服务器。

在生产环境强制执行迁移

某些迁移操作具有破坏性,可能导致数据丢失。为了保护你的生产数据库,执行这些命令时会提示确认。如果希望在生产环境中跳过确认提示直接执行,可以使用 --force 参数:

php artisan migrate --force

回滚迁移

要回滚最近一次的迁移操作,可以使用 Artisan 命令 rollback。该命令会回滚最后一个“批次”(batch)的迁移操作,这个批次可能包含多个迁移文件:

php artisan migrate:rollback

你可以通过 --step 选项限制回滚的迁移数量。例如,以下命令会回滚最近的五个迁移:

php artisan migrate:rollback --step=5

还可以通过 --batch 选项回滚指定批次的迁移,其中批次号对应 migrations 数据表中的 batch 值。例如,回滚批次 3 的所有迁移:

php artisan migrate:rollback --batch=3

如果你想查看迁移将要执行的 SQL 语句而不实际运行迁移,可以使用 --pretend 参数:

php artisan migrate:rollback --pretend

migrate:reset 命令会回滚应用中的所有迁移:

php artisan migrate:reset

使用单条命令回滚并重新迁移

migrate:refresh 命令会回滚所有迁移,然后重新执行 migrate 命令,相当于重新创建整个数据库:

php artisan migrate:refresh

# 刷新数据库并运行所有数据库填充(seeds)
php artisan migrate:refresh --seed

你也可以通过 --step 选项限制回滚和重新迁移的数量。例如,以下命令会回滚并重新迁移最近的五个迁移:

php artisan migrate:refresh --step=5

删除所有表并重新迁移

migrate:fresh 命令会删除数据库中的所有表,然后重新执行 migrate 命令:

php artisan migrate:fresh

php artisan migrate:fresh --seed

默认情况下,migrate:fresh 仅删除默认数据库连接的表。但你可以使用 --database 选项指定要迁移的数据库连接,连接名称应对应应用数据库配置文件中的定义:

php artisan migrate:fresh --database=admin
migrate:fresh 会删除数据库中所有表(无论表前缀如何)。在共享数据库的开发环境中使用此命令时需格外谨慎。

数据表

创建数据表

要创建一个新的数据库表,可以使用 Schema 门面的 create 方法。create 方法接受两个参数:第一个是表名,第二个是一个闭包函数(closure),该闭包接收一个 Blueprint 对象,可用于定义新表的结构:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::create('users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email');
    $table->timestamps();
});

在创建表时,你可以使用 Schema 构建器提供的各种列方法来定义表的字段。

判断表或字段是否存在

可以使用 hasTablehasColumnhasIndex 方法来判断表、字段或索引是否存在:

if (Schema::hasTable('users')) {
    // "users" 表存在
}

if (Schema::hasColumn('users', 'email')) {
    // "users" 表存在且包含 "email" 列
}

if (Schema::hasIndex('users', ['email'], 'unique')) {
    // "users" 表存在,并在 "email" 列上有唯一索引
}

数据库连接和表选项

如果需要在非默认数据库连接上执行 Schema 操作,可以使用 connection 方法:

Schema::connection('sqlite')->create('users', function (Blueprint $table) {
    $table->id();
});

此外,还有一些属性和方法可以用来定义表的其他选项:

  • 指定存储引擎(MariaDB/MySQL)
Schema::create('users', function (Blueprint $table) {
    $table->engine('InnoDB');
    // ...
});
  • 指定字符集和排序规则(MariaDB/MySQL)
Schema::create('users', function (Blueprint $table) {
    $table->charset('utf8mb4');
    $table->collation('utf8mb4_unicode_ci');
    // ...
});
  • 创建临时表 临时表只对当前数据库连接会话可见,并在连接关闭时自动删除:
Schema::create('calculations', function (Blueprint $table) {
    $table->temporary();
    // ...
});
  • 为表添加注释 表注释目前仅支持 MariaDB、MySQL 和 PostgreSQL:
Schema::create('calculations', function (Blueprint $table) {
    $table->comment('Business calculations');
    // ...
});

更新数据表

Schema 门面的 table 方法可用于修改已有的数据表。与 create 方法类似,table 方法接受两个参数:表名和一个闭包函数(closure),闭包接收一个 Blueprint 实例,你可以在闭包中向表中添加列或索引:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('users', function (Blueprint $table) {
    $table->integer('votes');
});

重命名 / 删除数据表

要重命名已有的数据库表,可以使用 rename 方法:

use Illuminate\Support\Facades\Schema;

Schema::rename($from, $to);

要删除已有的表,可以使用 dropdropIfExists 方法:

Schema::drop('users');

Schema::dropIfExists('users');

带外键的表重命名

在重命名表之前,应确保表上的外键约束在迁移文件中有明确的名称,而不是让 Laravel 使用默认命名规则。否则,外键约束名称仍会引用旧表名,可能导致问题。

表字段

创建字段

Schema 门面的 table 方法可用于修改已有的数据表。与 create 方法类似,table 方法接受两个参数:表名和一个闭包函数(closure),闭包接收一个 Illuminate\Database\Schema\Blueprint 实例,你可以在闭包中向表中添加列:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('users', function (Blueprint $table) {
    $table->integer('votes');
});

可用字段类型

Schema 构建器的 Blueprint 提供了多种方法,对应于可以添加到数据库表中的不同类型的列。下面的表列出了所有可用的方法:

(省略)

字段修饰器

除了上面列出的列类型之外,在向数据库表添加列时,还可以使用多种列“修饰器”。例如,如果希望某列允许 NULL 值,可以使用 nullable 方法:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('users', function (Blueprint $table) {
    $table->string('email')->nullable();
});

下表列出了所有可用的列修饰器(不包括索引修饰器):

修饰器描述
->after('column')将列放在另一列之后(MariaDB / MySQL)
->autoIncrement()将 INTEGER 列设置为自增(通常作为主键)
->charset('utf8mb4')为列指定字符集(MariaDB / MySQL)
->collation('utf8mb4_unicode_ci')为列指定排序规则
->comment('my comment')为列添加注释(MariaDB / MySQL / PostgreSQL)
->default($value)为列指定默认值
->first()将列放置为表中的第一列(MariaDB / MySQL)
->from($integer)设置自增列的起始值(MariaDB / MySQL / PostgreSQL)
->invisible()使列在 SELECT * 查询中“不可见”(MariaDB / MySQL)
->nullable($value = true)允许列插入 NULL
->storedAs($expression)创建存储生成列(MariaDB / MySQL / PostgreSQL / SQLite)
->unsigned()将 INTEGER 列设置为无符号(MariaDB / MySQL)
->useCurrent()将 TIMESTAMP 列的默认值设为 CURRENT_TIMESTAMP
->useCurrentOnUpdate()当记录更新时,TIMESTAMP 列使用 CURRENT_TIMESTAMP(MariaDB / MySQL)
->virtualAs($expression)创建虚拟生成列(MariaDB / MySQL / SQLite)
->generatedAs($expression)创建具有指定序列选项的标识列(PostgreSQL)
->always()定义标识列序列值优先于输入的规则(PostgreSQL)

默认值表达式

default 修饰器可以接受一个值或 Illuminate\Database\Query\Expression 实例。使用 Expression 可以防止 Laravel 将值加上引号,并允许使用数据库特定函数。例如,为 JSON 列指定默认值时非常有用:

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Migrations\Migration;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('flights', function (Blueprint $table) {
            $table->id();
            $table->json('movies')->default(new Expression('(JSON_ARRAY())'));
            $table->timestamps();
        });
    }
};
默认表达式的支持取决于数据库驱动、版本以及字段类型,请参考数据库文档。

列顺序

在 MariaDB 或 MySQL 中,可以使用 after 方法将列添加到已有列之后:

$table->after('password', function (Blueprint $table) {
    $table->string('address_line1');
    $table->string('address_line2');
    $table->string('city');
});

修改字段

change 方法允许修改已有列的类型和属性。例如,你可能希望增加一个字符串列的长度。下面的例子将 name 列的长度从 25 增加到 50:

Schema::table('users', function (Blueprint $table) {
    $table->string('name', 50)->change();
});

在修改列时,必须显式保留你希望保留的所有修饰器——任何未包含的属性都会被删除。例如,如果希望保留 unsigneddefaultcomment 属性,需要在修改列时显式调用每个修饰器:

Schema::table('users', function (Blueprint $table) {
    $table->integer('votes')->unsigned()->default(1)->comment('my comment')->change();
});

change 方法不会更改列的索引。因此,如果需要在修改列时显式添加或删除索引,可以使用索引修饰器:

// 添加索引...
$table->bigIncrements('id')->primary()->change();

// 删除索引...
$table->char('postal_code', 10)->unique(false)->change();

重命名字段

要重命名列,可以使用 Schema 构建器提供的 renameColumn 方法:

Schema::table('users', function (Blueprint $table) {
    $table->renameColumn('from', 'to');
});

删除字段

要删除列,可以使用 Schema 构建器的 dropColumn 方法:

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn('votes');
});

你也可以通过传入列名数组来一次性删除多个列:

Schema::table('users', function (Blueprint $table) {
    $table->dropColumn(['votes', 'avatar', 'location']);
});

可用的快捷删除命令:

命令描述
$table->dropMorphs('morphable');删除 morphable_idmorphable_type
$table->dropRememberToken();删除 remember_token
$table->dropSoftDeletes();删除 deleted_at
$table->dropSoftDeletesTz();dropSoftDeletes() 的别名
$table->dropTimestamps();删除 created_atupdated_at
$table->dropTimestampsTz();dropTimestamps() 的别名

索引

创建索引

Laravel 的 Schema 构建器支持多种类型的索引。下面的示例创建了一个新的 email 列,并指定其值必须唯一。要创建索引,可以在列定义后链式调用 unique 方法:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('users', function (Blueprint $table) {
    $table->string('email')->unique();
});

另外,你也可以在定义列之后创建索引。此时,应在 Schema 构建器的 Blueprint 上调用 unique 方法,该方法接受要添加唯一索引的列名:

$table->unique('email');

你还可以向索引方法传入列名数组,以创建复合索引(compound 或 composite index):

$table->index(['account_id', 'created_at']);

创建索引时,Laravel 会根据表名、列名和索引类型自动生成索引名称,但你也可以通过方法的第二个参数手动指定索引名称:

$table->unique('email', 'unique_email');

可用索引类型

Laravel 的 Schema 构建器 Blueprint 类提供了创建各种索引的方法。每个索引方法都可以接受一个可选的第二个参数来指定索引名称,如果省略,索引名称将根据表名、列名及索引类型自动生成。可用索引方法如下表所示:

命令描述
$table->primary('id');添加主键
$table->primary(['id', 'parent_id']);添加复合主键
$table->unique('email');添加唯一索引
$table->index('state');添加普通索引
$table->fullText('body');添加全文索引(MariaDB / MySQL / PostgreSQL)
$table->fullText('body')->language('english');为指定语言添加全文索引(PostgreSQL)
$table->spatialIndex('location');添加空间索引(不支持 SQLite)

重命名索引

要重命名索引,可以使用 Schema 构建器 Blueprint 提供的 renameIndex 方法。该方法的第一个参数为当前索引名称,第二个参数为新的索引名称:

$table->renameIndex('from', 'to');

删除索引

要删除索引,必须指定索引的名称。默认情况下,Laravel 会根据表名、索引列名和索引类型自动生成索引名称。示例如下:

命令描述
$table->dropPrimary('users_id_primary');删除 "users" 表的主键索引。
$table->dropUnique('users_email_unique');删除 "users" 表的唯一索引。
$table->dropIndex('geo_state_index');删除 "geo" 表的普通索引。
$table->dropFullText('posts_body_fulltext');删除 "posts" 表的全文索引。
$table->dropSpatialIndex('geo_location_spatialindex');删除 "geo" 表的空间索引(SQLite 除外)。

如果在删除索引的方法中传入一个列名数组,Laravel 将根据表名、列名和索引类型自动生成约定的索引名称:

Schema::table('geo', function (Blueprint $table) {
    $table->dropIndex(['state']); // 删除索引 'geo_state_index'
});

外键约束

Laravel 还支持创建外键约束,用于在数据库层面强制保持参照完整性。例如,在 posts 表上定义一个 user_id 列,引用 users 表的 id 列:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

Schema::table('posts', function (Blueprint $table) {
    $table->unsignedBigInteger('user_id');

    $table->foreign('user_id')->references('id')->on('users');
});

由于这种写法较为冗长,Laravel 提供了更简洁的方法,利用约定来提升开发体验。使用 foreignId 方法创建列后,上例可以简化为:

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained();
});
  • foreignId 方法创建一个无符号的 BIGINT 列。
  • constrained 方法根据约定确定被引用的表和列。

如果表名不符合 Laravel 的约定,可以手动传入:

Schema::table('posts', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained(
        table: 'users', indexName: 'posts_user_id'
    );
});

还可以为外键约束指定 on deleteon update 操作:

$table->foreignId('user_id')
    ->constrained()
    ->onUpdate('cascade')
    ->onDelete('cascade');

Laravel 也提供了更具可读性的链式方法:

方法描述
$table->cascadeOnUpdate();更新时级联。
$table->restrictOnUpdate();更新时受限。
$table->nullOnUpdate();更新时将外键值设为 NULL。
$table->noActionOnUpdate();更新时不执行操作。
$table->cascadeOnDelete();删除时级联。
$table->restrictOnDelete();删除时受限。
$table->nullOnDelete();删除时将外键值设为 NULL。
$table->noActionOnDelete();如果存在子记录则阻止删除。

注意:任何额外的列修饰符必须在调用 constrained 方法之前使用:

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

删除外键

使用 dropForeign 方法删除外键约束,需要传入外键约束名。外键约束名遵循与索引相同的命名约定,即:表名 + 列名 + _foreign 后缀:

$table->dropForeign('posts_user_id_foreign');

也可以传入包含列名的数组,Laravel 会根据约定自动生成外键约束名:

$table->dropForeign(['user_id']);

开关外键约束

在迁移中可以启用或禁用外键约束:

Schema::enableForeignKeyConstraints();

Schema::disableForeignKeyConstraints();

Schema::withoutForeignKeyConstraints(function () {
    // 此闭包内禁用外键约束...
});
SQLite 默认禁用外键约束,使用 SQLite 时,请在数据库配置中启用外键支持,否则迁移中无法创建外键。

事件

为了方便,每个迁移操作都会触发一个事件。以下所有事件都继承自基类 Illuminate\Database\Events\MigrationEvent

类名描述
Illuminate\Database\Events\MigrationsStarted一批迁移即将执行。
Illuminate\Database\Events\MigrationsEnded一批迁移已完成执行。
Illuminate\Database\Events\MigrationStarted单个迁移即将执行。
Illuminate\Database\Events\MigrationEnded单个迁移已完成执行。
Illuminate\Database\Events\NoPendingMigrations迁移命令未找到任何待执行的迁移。
Illuminate\Database\Events\SchemaDumped数据库架构导出已完成。
Illuminate\Database\Events\SchemaLoaded已加载现有的数据库架构导出。