迁移
介绍
迁移(Migrations)就像数据库的版本控制,允许你的团队定义并共享应用的数据库结构。如果你曾经在从版本控制拉取更新后,还需要告诉同事手动在本地数据库中添加某个字段,那么你就遇到了迁移所解决的问题。
Laravel 的 Schema 门面(Schema facade)提供了与数据库无关的支持,可以在 Laravel 所支持的所有数据库系统中创建和操作数据表。通常,迁移会使用这个门面来创建和修改数据库表及字段。
生成迁移
你可以使用 Artisan 命令 make:migration 来生成数据库迁移文件。新迁移文件会被放置在 database/migrations 目录下。每个迁移文件名都包含一个时间戳,Laravel 会根据时间戳来确定迁移的执行顺序:
php artisan make:migration create_flights_table
Laravel 会根据迁移名称尝试推断表名,以及该迁移是否用于创建新表。如果 Laravel 能够从迁移名称中确定表名,它会在生成的迁移文件中预先填充指定的表名。否则,你可以在迁移文件中手动指定表名。
如果你希望为生成的迁移文件指定自定义路径,可以在执行 make:migration 命令时使用 --path 选项。指定的路径应相对于应用的根路径。
合并迁移
随着应用的不断开发,你可能会积累越来越多的迁移文件,这会导致 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 文件提交到版本控制,这样团队中新加入的开发者可以快速创建应用的初始数据库结构。
迁移结构
迁移类包含两个方法:up 和 down。up 方法用于向数据库中添加新的表、字段或索引,而 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
在生产环境强制执行迁移
某些迁移操作具有破坏性,可能导致数据丢失。为了保护你的生产数据库,执行这些命令时会提示确认。如果希望在生产环境中跳过确认提示直接执行,可以使用 --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 构建器提供的各种列方法来定义表的字段。
判断表或字段是否存在
可以使用 hasTable、hasColumn 和 hasIndex 方法来判断表、字段或索引是否存在:
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);
要删除已有的表,可以使用 drop 或 dropIfExists 方法:
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();
});
在修改列时,必须显式保留你希望保留的所有修饰器——任何未包含的属性都会被删除。例如,如果希望保留 unsigned、default 和 comment 属性,需要在修改列时显式调用每个修饰器:
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_id 和 morphable_type 列 |
$table->dropRememberToken(); | 删除 remember_token 列 |
$table->dropSoftDeletes(); | 删除 deleted_at 列 |
$table->dropSoftDeletesTz(); | dropSoftDeletes() 的别名 |
$table->dropTimestamps(); | 删除 created_at 和 updated_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 delete 和 on 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 () {
// 此闭包内禁用外键约束...
});
事件
为了方便,每个迁移操作都会触发一个事件。以下所有事件都继承自基类 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 | 已加载现有的数据库架构导出。 |