数据库:迁移

简介

迁移就像数据库的版本控制一样,允许团队轻松修改并分享应用的数据库结构。迁移通常和 Laravel 的数据库结构生成器搭配使用,轻松构建数据库结构。如果您曾经遇到这样的问题,即必须告诉同事手动将字段添加到他们本地的数据库结构中,数据库迁移解决了该问题。

Laravel 的 Schema Facade 为在所有 Laravel 支持的数据库系统中创建和操作数据表提供了数据库无关的支持。

生成迁移

创建迁移,可以使用 Artisan 命令 make:migration

php artisan make:migration create_users_table

新的迁移会放在 database/migrations 目录中。每个迁移文件名都包含一个时间戳,Laravel 用它决定迁移的顺序。

--table--create 选项也可用于指定数据表的名称和迁移是否会创建一个新的数据表。这些选项会在生成的迁移文件中预先填充指定的数据表:

php artisan make:migration create_users_table --create=users

php artisan make:migration add_votes_to_users_table --table=users

如果要为生成的迁移指定自定义输出路径,可以在执行 make:migration 命令时使用 --path 选项。给定的路径应该相对于应用的基本路径。

迁移结构

迁移类包含两个方法:updownup 方法用于添加新的数据表,字段或索引到数据库,而 down 方法应该执行与 up 方法相反的操作。

在这两个方法中都可以使用 Laravel 的数据库结构生成器清晰地创建和修改数据表。要了解 Schema 生成器所有的可用方法,可以查看 其文档。例如,下列迁移示例创建一张 flights 表:

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

class CreateFlightsTable extends Migration
{
    /**
     * 运行迁移
     *
     * @return void
     */
    public function up()
    {
        Schema::create('flights', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('airline');
            $table->timestamps();
        });
    }

    /**
     * 撤销迁移
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('flights');
    }
}

运行迁移

要运行所有未完成的迁移,可以执行 Artisan 命令 migrate

php artisan migrate

如果使用 Homestead 虚拟机,应该在虚拟机中运行此命令。

生产环境下强制运行迁移

有些迁移操作是破坏性的,这意味着它们可能导致您丢失数据。为了保护您不在生产数据库中运行这些命令,在命令执行前会询问相关信息。如果要强制运行命令并不提示,可以使用 --force 标识:

php artisan migrate --force

回滚迁移

回滚到最近的迁移操作,可以使用 rollback 命令。此命令回滚最后「一批」迁移,可能包含多个迁移文件:

php artisan migrate:rollback

可以通过为 rollback 命令提供 step 选项回滚指定步数的迁移。例如,下列命令会回滚最后五次迁移:

php artisan migrate:rollback --step=5

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

php artisan migrate:reset

单个命令回滚 & 迁移

migrate:refresh 命令会回滚所有迁移,然后执行 migrate 命令。此命令高效地重建整个数据库:

php artisan migrate:refresh

// 恢复数据库并运行所有数据库填充
php artisan migrate:refresh --seed

可以通过为 refresh 命令提供 step 选项回滚并重新迁移指定步数的迁移。例如,下列命令会回滚并重新迁移最后五次迁移:

php artisan migrate:refresh --step=5

删除所有数据表 & 迁移

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

php artisan migrate:fresh

php artisan migrate:fresh --seed

数据表

创建数据表

创建新数据表,可以使用 Schema Facade 的 create 方法。create 方法接收两个参数。第一个参数是数据表名称,而第二个参数是一个接收用于定义新表的 Blueprint 对象的闭包:

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

当然,创建数据表时,可以使用数据库结构生成器的任何 字段方法 定义数据表字段。

检查数据表/字段是否存在

可以使用 hasTablehasColumn 方法轻松检查数据表或字段是否存在:

if (Schema::hasTable('users')) {
    //
}

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

数据库连接 & 数据表选项

如果要在不是默认连接的数据库连接上执行一个数据库结构操作,可以使用 connection 方法:

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

可以在数据库结构生成器上使用以下命令定义数据表选项:

命令 描述
$table->engine = 'InnoDB'; 指定数据表存储引擎 (MySQL)
$table->charset = 'utf8'; 为数据表指定默认字符集 (MySQL)
$table->collation = 'utf8_unicode_ci'; 为数据表指定默认排序规则 (MySQL)
$table->temporary(); 创建临时表 (不支持 SQL Server)

重命名/删除数据表

重命名已存在的数据表,可以使用 rename 方法:

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

删除已存在的数据表,可以使用 dropdropIfExists 方法:

Schema::drop('users');

Schema::dropIfExists('users');

重命名带外键的数据表

在重命名数据表之前,应该验证数据表上的任何外键约束在迁移文件中都有明确的名称,而不是让 Laravel 指定一个约定的名称。否则,外键约束名称会引用旧的数据表名称。

字段

创建字段

Schema Facade 的 table 方法可用于更新已存在的数据表。与 create 方法一样,table 方法接收两个参数:数据表的名称和一个接收用于添加字段到数据表的 Blueprint 实例的闭包:

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

可用的字段类型

当然,数据库结构生成器包含创建数据表时可以指定的各种字段类型:

命令 描述
$table->bigIncrements('id'); 自增 UNSIGNED BIGINT (主键)等效字段
$table->bigInteger('votes'); BIGINT 等效字段
$table->binary('data'); BLOB 等效字段
$table->boolean('confirmed'); BOOLEAN 等效字段
$table->char('name', 100); 带可选长度的 CHAR 等效字段
$table->date('created_at'); DATE 等效字段
$table->dateTime('created_at'); DATETIME 等效字段
$table->dateTimeTz('created_at'); DATETIME (带时区)等效字段
$table->decimal('amount', 8, 2); 带精度(总位数)和范围(小数位数)的 DECIMAL 等效字段
$table->double('amount', 8, 2); 带精度(总位数)和范围(小数位数)的 DOUBLE 等效字段
$table->enum('level', ['easy', 'hard']); ENUM 等效字段
$table->float('amount', 8, 2); 带精度(总位数)和范围(小数位数)的 FLOAT 等效字段
$table->geometry('positions'); GEOMETRY 等效字段
$table->geometryCollection('positions'); GEOMETRYCOLLECTION 等效字段
$table->increments('id'); 自增 UNSIGNED INTEGER (主键)等效字段
$table->integer('votes'); INTEGER 等效字段
$table->ipAddress('visitor'); IP 地址等效字段
$table->json('options'); JSON 等效字段
$table->jsonb('options'); JSONB 等效字段
$table->lineString('positions'); LINESTRING 等效字段
$table->longText('description'); LONGTEXT 等效字段
$table->macAddress('device'); MAC 地址等效字段
$table->mediumIncrements('id'); 自增 UNSIGNED MEDIUMINT (主键)等效字段
$table->mediumInteger('votes'); MEDIUMINT 等效字段
$table->mediumText('description'); MEDIUMTEXT 等效字段
$table->morphs('taggable'); 添加 taggable_id UNSIGNED BIGINT 和 taggable_type VARCHAR 等效字段
$table->multiLineString('positions'); MULTILINESTRING 等效字段
$table->multiPoint('positions'); MULTIPOINT 等效字段
$table->multiPolygon('positions'); MULTIPOLYGON 等效字段
$table->nullableMorphs('taggable'); 添加可为空的 morphs() 字段
$table->nullableTimestamps(); timestamps() 方法的别名
$table->point('position'); POINT 等效字段
$table->polygon('positions'); POLYGON 等效字段
$table->rememberToken(); 添加可为空的 remember_token VARCHAR(100) 等效字段
$table->smallIncrements('id'); 自增 UNSIGNED SMALLINT (主键)等效字段
$table->smallInteger('votes'); SMALLINT 等效字段
$table->softDeletes(); 为软删除添加可为空的 deleted_at TIMESTAMP 等效字段
$table->softDeletesTz(); 为软删除添加可为空的 deleted_at TIMESTAMP (带时区)等效字段
$table->string('name', 100); 带可选长度的 VARCHAR 等效字段
$table->text('description'); TEXT 等效字段
$table->time('sunrise'); TIME 等效字段
$table->timeTz('sunrise'); TIME (带时区)等效字段
$table->timestamp('added_on'); TIMESTAMP 等效字段
$table->timestampTz('added_on'); TIMESTAMP (带时区)等效字段
$table->timestamps(); 添加可为空的 created_atupdated_at TIMESTAMP 等效字段
$table->timestampsTz(); 添加可为空的 created_atupdated_at TIMESTAMP (带时区)等效字段
$table->tinyIncrements('id'); 自增 UNSIGNED TINYINT (主键)等效字段
$table->tinyInteger('votes'); TINYINT 等效字段
$table->unsignedBigInteger('votes'); UNSIGNED BIGINT 等效字段
$table->unsignedDecimal('amount', 8, 2); 带精度(总位数)和范围(小数位数)的 UNSIGNED DECIMAL 等效字段
$table->unsignedInteger('votes'); UNSIGNED INTEGER 等效字段
$table->unsignedMediumInteger('votes'); UNSIGNED MEDIUMINT 等效字段
$table->unsignedSmallInteger('votes'); UNSIGNED SMALLINT 等效字段
$table->unsignedTinyInteger('votes'); UNSIGNED TINYINT 等效字段
$table->uuid('id'); UUID 等效字段
$table->year('birth_year'); YEAR 等效字段

字段修饰

除了添加上述列出的字段类型外,在添加字段到数据库时还有几个可以使用的字段「修饰方法」。例如,要让字段「可空」,可以使用 nullable 方法:

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

以下是所有可用的字段修饰方法列表。此列表中不包括 索引修饰方法

修饰方法 描述
->after('column') 将字段放在另一个字段「之后」(MySQL)
->autoIncrement() 将 INTEGER 字段设置为自增(主键)
->charset('utf8') 为字段指定字符集(MySQL)
->collation('utf8_unicode_ci') 为字段指定排序规则(MySQL/SQL Server)
->comment('my comment') 为字段添加注释(MySQL)
->default($value) 为字段指定「默认」值
->first() 将字段放在数据表的「首位」(MySQL)
->nullable($value = true) (默认情况下)允许插入 NULL 值到字段中
->storedAs($expression) 创建一个存储生成的字段(MySQL)
->unsigned() 将 INTEGER 字段设置为 UNSIGNED(MySQL)
->useCurrent() 设置 TIMESTAMP 字段使用 CURRENT_TIMESTAMP 作为默认值
->virtualAs($expression) 创建一个虚拟生成的字段(MySQL)
->generatedAs($expression) 使用指定的序列选项生成 ID 字段(PostgreSQL)
->always() 为 ID 字段定义优先序列值覆盖输入(PostgreSQL)

修改字段

前提

修改字段之前,确保在 composer.json 文件中添加了 doctrine/dbal 依赖。Doctrine DBAL 库用于判断字段的当前状态并创建字段进行对应调整所需的 SQL 语句:

composer require doctrine/dbal

更新字段属性

change 方法用于将某些已存在的字段类型修改为新的类型或者修改字段的属性。例如,可能希望增加字符串字段的长度。为了在实际操作中查看 change 方法,我们将 name 字段的长度从 25 增加到 50:

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

也可以将字段修改为可空:

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

只有如下字段类型可以被「更改」:bigInteger,inary,boolean,date,dateTime,dateTimeTz,decimal,integer,json,longText,mediumText,smallInteger,string,text,time,unsignedBigInteger,unsignedInteger 和 unsignedSmallInteger。

重命名字段

重命名字段,可以在数据库结构生成器上使用 renameColumn 方法。在重命名字段前,确保在 composer.json 文件中添加了 doctrine/dbal 依赖:

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

可以重命名数据表中的任何字段,但目前不支持 enum 类型的字段。

删除字段

删除字段,可以在数据库结构生成器上使用 dropColumn 方法。从 SQLite 数据库删除字段前,将需要在 composer.json 文件中添加 doctrine/dbal 依赖并在终端中运行 composer update 命令安装此库:

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

还可以传递一个字段名数组给 dropColumn 方法,从数据表中删除多个字段:

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

使用 SQLite 数据库时,不支持在单个迁移中删除或修改多个字段。

可用的命令别名

命令 描述
$table->dropRememberToken(); 删除 remember_token 字段
$table->dropSoftDeletes(); 删除 deleted_at 字段
$table->dropSoftDeletesTz(); dropSoftDeletes() 方法的别名
$table->dropTimestamps(); 删除 created_atupdated_at 字段
$table->dropTimestampsTz(); dropTimestamps() 方法的别名

索引

创建索引

数据库结构生成器支持几种索引类型。首先,我们看一个指定字段值为唯一键的实例。要创建该索引,我们可以在字段定义上链式调用 unique 方法:

$table->string('email')->unique();

或者,在定义字段后创建索引。例如:

$table->unique('email');

甚至还可以传递一个字段数组到索引方法,创建一个复合(或混合)索引:

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

Laravel 会自动生成一个适当的索引名称,不过也可以传递第二个参数自己指定名称:

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

可用的索引类型

每个索引方法都接收一个可选的第二个参数来指定索引名称。如果省略,该名称会采用表名和字段名:

命令 描述
$table->primary('id'); 添加主键
$table->primary(['id', 'parent_id']); 添加符合主键
$table->unique('email'); 添加唯一索引
$table->index('state'); 添加普通索引
$table->spatialIndex('location'); 添加空间索引(SQLite 除外)

索引长度 & MySQL/MariaDB

Laravel 默认使用 utf8mb4 字符集,支持在数据库中存储「emojis」表情。如果使用 MySQL 5.7.7 之前的版本或 MariaDB 10.2.2 之前的版本,需要手动配置默认迁移生成的字符串长度以便 MySQL 为其创建索引。可以在 AppServiceProvider 中调用 Schema::defaultStringLength 方法进行配置:

use Illuminate\Support\Facades\Schema;

/**
 * 启动任何应用服务
 *
 * @return void
 */
public function boot()
{
    Schema::defaultStringLength(191);
}

或者,可以为数据库启动 innodb_large_prefix 选项。参考数据库文档有关如何正确地启用此选项的说明。

重命名索引

重命名索引,可以使用 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->dropSpatialIndex('geo_location_spatialindex'); 从「geo」数据表中删除空间索引(SQLite 除外)

如果传递一个字段数组给方法来删除索引,会基于数据表名称,字段名称和键的类型生成约定的索引名称:

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

外键约束

Laravel 也支持创建外键约束,用于在数据库层面强制引用完整性。例如,我们在 posts 数据表上定义一个 user_id 字段,它引用 users 数据表上的 id 字段:

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

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

还可以为约束的「on delete」和「on update」特性指定想要的操作:

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

删除外键,可以使用 dropForeign 方法。外键约束和索引使用相同的命名约定。因此,我们会连接约束的数据表名称和字段名称,然后在后面接上「_foreign」:

$table->dropForeign('posts_user_id_foreign');

或者,在删除时传递一个数组值,自动使用约定的约束名称:

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

可以在迁移中使用如下方法启用或禁用外键约束:

Schema::enableForeignKeyConstraints();

Schema::disableForeignKeyConstraints();

SQLite 默认禁用外键约束。使用 SQLite 时,在迁移中创建外键之前确保启用了外键支持。