简介 迁移就像是对数据库进行的版本控制,让你的团队能够轻松地去定义和共享程序的数据库结构。迁移通常配合 Laravel 的结构生成器,可以轻松生成应用程序的数据库结构。如果团队中有个成员在他本地的数据库环境中手动的添加了某个字段,那么你将会面对如何解决数据库迁移的问题。 Laravel 的 Schema facade 提供了数据库相关的支持,可以在所有 Laravel 支持的数据库管理系统中创建和操作表。 生成迁移 使用 make:migration Artisan 命令 命令来创建迁移: ``` php artisan make:migration create_users_table ``` 新的迁移文件将会放在 database/migrations 目录下。 所有的迁移文件名称都会包含一个时间戳,Laravel 将据此决定迁移文件运行的顺序。 提示:迁移 stubs 必须使用 stub publishing 进行自定义。 --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 参数。 给定的路径是以应用程序文件夹为相对路径。 迁移转储(迁移合并) 随着时间,你构建的应用程序中的迁移会积累的越来越多。这会使目录变得臃肿,甚至会有数百个迁移文件。所以,如果你愿意,你可以将若干个迁移文件,压缩到单个 SQL 文件中。进行这个操作,需要执行的命令是:schema:dump。 示例: ``` php artisan schema:dump // 上面示例为转储但不删除原有迁移文件,下面示例为转储且删除原有迁移文件 php artisan schema:dump --prune ``` 在执行以上命令后,laravel 将会把转储后的 SQL 文件,放置到您项目的 database/schema 目录中。 在进行转储操作后,当您尝试进行迁移操作时,在未指定迁移文件的默认情况下,Laravel 将会首先执行 SQL 文件的内容。在执行 SQL 文件后,Laravel 将会继续执行其他迁移文件。 数据库迁移的核心作用是,使团队中的其他开发人员,可以快速的创建该项目的初始数据库结构。 所以在完成项目的数据库架构修改后,您应该将数据库架构文件提交至代码管理员、项目负责人或是 Git 类项目共享平台,这将大大提高您项目团队的效率和共协能力。 注意:迁移转储功能仅适用于 MySQL,PostgreSQL 和 SQLite 数据库。 迁移的操作方法 面对的迁移的操作有两个方法: up 和 down。方法 up 的作用是向数据库中添加新的表或列或索引,而 down 的方法,就是 up 方法的反向操作。 在这两种方法中,您可以使用 Laravel 的 Schema 生成器显式地创建和修改表。要了解 Schema 生成器上可用的所有方法,请查看其文档。例如,以下迁移将创建一个 flights 表: ``` <?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateFlightsTable extends Migration { /** * 运行迁移 * * @return void */ public function up() { Schema::create('flights', function (Blueprint $table) { $table->id(); $table->string('name'); $table->string('airline'); $table->timestamps(); }); } /** * 回滚迁移 * * @return void */ public function down() { Schema::drop('flights'); } } ``` 执行迁移 执行以下 migrate Artisan 命令,来执行未执行过的迁移: ``` 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 命令将会回滚您的所有迁移操作,然后 Laravel 将自己继续执行 migrate 命令进行重新迁移。同时可在该命令后跟用 --seed, 这个参数的作用是在 “刷新” 完成数据库之后,对后面新增的迁移文件进行迁移。 示例: ``` php artisan migrate:refresh // 上列是刷新当前已有的迁移,下列是刷新当前已有的迁移后,更新未有的迁移 php artisan migrate:refresh --seed ``` 在 migrate:refresh 中,您还可以使用 step 参数来一次性选择回滚的次数。例如,以下命令将回滚并重新迁移最后五次迁移: ``` php artisan migrate:refresh --step=5 ``` 使用一个命令同时进行删除和迁移操作 使用 migrate:fresh 命令将会删除您已迁移的数据表,然后 Laravel 将自己继续执行 migrate 命令。同时可在该命令后跟用 --seed, 这个参数的作用是在 “刷新” 完成数据库之后,对后面新增的迁移文件进行迁移。 示例: ``` php artisan migrate:fresh // 上列是刷新当前已有的迁移,下列是刷新当前已有的迁移后,更新未有的迁移。 php artisan migrate:fresh --seed ``` 数据表 创建数据表 接下来我们将使用 Schema 的 create 方法,创建一个新的数据表。 create 方法中可以接受两个参数,一个是表格的名称,第二个是 Closure,作用是可以在定义一个新数据表的时候接受 Blueprint 对象。 示例: ``` Schema::create('users', function (Blueprint $table) { $table->id(); }); ``` 您在创建数据表时,在如上示例花括号中,可以使用结构生成器的任何 字段方法 去定义数据表的字段。 检查表 / 列是否存在 您可以使用 hasTable 和 hasColumn 方法来检测表 / 列是否存在: ``` if (Schema::hasTable('users')) { # 如果存在 users 表则执行 // } if (Schema::hasColumn('users', 'email')) { # 如果存在 users, email 列则执行 // } ``` 数据库连接 & 数据表选项 如果要对非默认连接的数据库连接执行结构操作,可以使用 connection 方法: ``` Schema::connection('foo')->create('users', function (Blueprint $table) { $table->id(); }); ``` 你也可以在数据库结构生成器上使用以下命令来定义表的选项: ``` Command Description $table->engine = 'InnoDB'; 指定表存储引擎 (MySQL)。 $table->charset = 'utf8mb4'; 指定数据表的默认字符集 (MySQL)。 $table->collation = 'utf8mb4_unicode_ci'; 指定数据表默认的排序规则 (MySQL)。 $table->temporary(); 创建临时表 (不支持 SQL Server)。 ``` 重命名 / 删除数据表 若要重命名数据表,可以使用 rename 方法: ``` Schema::rename($from, $to); ``` 删除一个已存在的数据表, 可使用 drop 或 dropIfExists 方法: ``` Schema::drop('users'); Schema::dropIfExists('users'); ``` 重命名带外键的数据表 在重命名表之前,你应该验证表上的任何外键约束在迁移文件中都有明确的名称,而不是让 Laravel 按照约定来设置一个名称。否则,外键的约束名称将引用旧表名。 字段 创建字段 使用 Schema facade 的 table 方法可以更新现有的数据表。如同 create 方法一样,table 方法会接受两个参数:一个是表名,另一个则是接收可以用来向表中添加字段的 Blueprint 实例的闭包: ``` Schema::table('users', function (Blueprint $table) { $table->string('email'); }); ``` 可用的字段类型 数据库结构生成器包含构建表时可以指定的各种字段类型: |命令 |说明| |----|---| |$table->id(); |$table->bigIncrements('id') 的别名| |$table->foreignId('user_id'); |$table->unsignedBigInteger('user_id') 的别名| |$table->bigIncrements('id'); |递增 ID(主键),相当于「UNSIGNED BIG INTEGER| |$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', 0); |相当于 DATETIME ,可以指定位数| |$table->dateTimeTz('created_at', 0); |相当于 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'); |递增 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'); |递增 ID(主键),相当于 UNSIGNED MEDIUMINT| |$table->mediumInteger('votes'); |相当于 MEDIUMINT| |$table->mediumText('description'); |相当于 MEDIUMTEXT| |$table->morphs('taggable'); |相当于加入递增 UNSIGNED BIGINT 类型的 taggable_id 与字符串类型的 taggable_type| |$table->uuidMorphs('taggable'); |相当于添加一个 CHAR (36) 类型的 taggable_id 字段和 VARCHAR (255) UUID 类型的 taggable_type| |$table->multiLineString('positions'); |相当于 MULTILINESTRING| |$table->multiPoint('positions'); |相当于 MULTIPOINT| |$table->multiPolygon('positions');| 相当于 MULTIPOLYGON| |$table->nullableMorphs('taggable'); |添加一个可以为空版本的 morphs() 字段.| |$table->nullableUuidMorphs('taggable'); |添加一个可以为空版本的 uuidMorphs() 字段| |$table->nullableTimestamps(0); timestamps() |方法的别名| |$table->point('position'); |相当于 POINT| |$table->polygon('positions'); |相当于 POLYGON| |$table->rememberToken(); |添加一个允许空值的 VARCHAR (100) 类型的 remember_token 字段| |$table->set('flavors', ['strawberry', 'vanilla']); |相当于 SET| |$table->smallIncrements('id'); |递增 ID(主键),相当于 UNSIGNED SMALLINT| |$table->smallInteger('votes'); |相当于 SMALLINT| |$table->softDeletes('deleted_at', 0); |相当于为软删除添加一个可空的 deleted_at 字段| |$table->softDeletesTz('deleted_at', 0); |相当于为软删除添加一个可空的 带时区的 deleted_at 字段| |$table->string('name', 100); |相当于指定长度的 VARCHAR| |$table->text('description'); |相当于 TEXT| |$table->time('sunrise', 0); |相当于指定位数的 TIME| |$table->timeTz('sunrise', 0); |相当于指定位数带时区的 TIME| |$table->timestamp('added_on', 0); |相当于指定位数的 TIMESTAMP| |$table->timestampTz('added_on', 0); |相当于指定位数带时区的 TIMESTAMP| |$table->timestamps(0); |相当于添加可空的 TIMESTAMP 类型的 created_at 和 updated_at| |$table->timestampsTz(0); |相当于添加指定时区的可空的 TIMESTAMP 类型的 created_at 和 updated_at| |$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(); }); ``` 以下是所有可用的字段修饰符的列表。此列表不包括 索引修饰符: |Modifier| Description| |----|-----| |->after('column') |将此字段放置在其它字段 「之后」 (MySQL)| |->autoIncrement() |将 INTEGER 类型的字段设置为自动递增的主键| |->charset('utf8mb4') |指定一个字符集 (MySQL)| |->collation('utf8mb4_unicode_ci') |指定排序规则 (MySQL/PostgreSQL/SQL Server)| |->comment('my comment') |为字段增加注释 (MySQL/PostgreSQL)| |->default($value) |为字段指定 “默认” 值| |->first() |将此字段放置在数据表的 「首位」 (MySQL)| |->from($integer) |给自增字段设置一个起始值 (MySQL / PostgreSQL)| |->nullable($value = true) |此字段允许写入 NULL 值(默认情况下)| |->storedAs($expression) |创建一个存储生成的字段 (MySQL)| |->unsigned() |设置 INTEGER 类型的字段为 UNSIGNED (MySQL)| |->useCurrent() |将 TIMESTAMP 类型的字段设置为使用 CURRENT_TIMESTAMP 作为默认值| |->virtualAs($expression) |创建一个虚拟生成的字段 (MySQL)| |->generatedAs($expression) |使用指定的序列生成标识列(PostgreSQL)| |->always() |定义序列值优先于标识列的输入 (PostgreSQL)| 默认值表达式 default 修饰符接收一个变量或者一个 \Illuminate\Database\Query\Expression 实例。使用 Expression 实例可以避免使用包含在引号中的值,并且允许你使用特定数据库函数。这在当你需要给 JSON 字段指定默认值的时候特别有用: ``` <?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Query\Expression; use Illuminate\Database\Migrations\Migration; class CreateFlightsTable extends Migration { /** * 运行迁移 * * @return void */ public function up() { Schema::create('flights', function (Blueprint $table) { $table->id(); $table->json('movies')->default(new Expression('(JSON_ARRAY())')); $table->timestamps(); }); } } ``` 注意:支持哪些默认值的表示方式取决于你的数据库驱动、数据库版本、还有字段类型。请参考合适的文档使用。还有一点要注意的是,使用数据库特定函数,可能会将你绑牢到特定的数据库驱动上。 先决条件 在修饰字段之前,请确保将 doctrine/dbal 依赖添加到 composer.json 文件中。Doctrine DBAL 库用于确定字段的当前状态, 并创建对该字段进行指定调整所需的 SQL 查询: ``` composer require doctrine/dbal ``` 修改字段 更新字段属性 change 方法可以将现有的字段类型修改为新的类型或修改属性。 比如,你可能想增加。字符串字段的长度,可以使用 change 方法把 name 字段的长度从 25 增加到 50: ``` Schema::table('users', function (Blueprint $table) { $table->string('name', 50)->change(); }); ``` 我们同样可以使用 nullable 将字段修改为允许为空: ``` Schema::table('users', function (Blueprint $table) { $table->string('name', 50)->nullable()->change(); }); ``` 注意:只有以下字段类型能被 「修改」:bigInteger、binary、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->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() 方法别名| 索引 创建索引 结构生成器支持多种类型的索引。下面的例子中新建了一个值唯一的 email 字段。我们可以将 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 。如果你是在版本低于 5.7.7 的 MySQL 或者版本低于 10.2.2 的 MariaDB 上创建索引,那你就需要手动配置数据库迁移的默认字符串长度。即在 AppServiceProvider 中调用 Schema::defaultStringLength 方法来配置它: ``` use Illuminate\Support\Facades\Schema; /** * Bootstrap any application services. * * @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」 表中删除 unique 索引| |$table->dropIndex('geo_state_index'); |从 「geo」 表中删除基本索引| |$table->dropSpatialIndex('geo_location_spatialindex'); |从 「geo」 表中删除空间索引(不支持 SQLite)| 如果将字段数组传给 dropIndex 方法,会删除根据表名、字段和键类型生成的索引名称。 ``` Schema::table('geo', function (Blueprint $table) { $table->dropIndex(['state']); // 删除 'geo_state_index' 索引 }); ``` 外键约束 Laravel 还支持创建用于在数据库层中的强制引用完整性的外键约束。例如,让我们在 posts 表上定义一个引用 users 表的 id 字段的 user_id 字段: ``` Schema::table('posts', function (Blueprint $table) { $table->unsignedBigInteger('user_id'); $table->foreign('user_id')->references('id')->on('users'); }); ``` 由于这种外键约束的定义方式过于繁复,Laravel 额外提供了更简洁的方法,这些方法使用约定来提供更好的开发人员体验。上面的示例还可以这么写: ``` Schema::table('posts', function (Blueprint $table) { $table->foreignId('user_id')->constrained(); }); foreignId 方法是 unsignedBigInteger 的别名,而 constrained 方法将使用约定来确定所引用的表名和列名。如果表名与约定不匹配,可以通过将表名作为参数传递给 constrained 方法来指定表名: Schema::table('posts', function (Blueprint $table) { $table->foreignId('user_id')->constrained('users'); }); ``` 你可以为约束的「on delete」和「on update」属性指定所需的操作: ``` $table->foreignId('user_id') ->constrained() ->onDelete('cascade'); ``` 当使用任意 字段修饰符 的时候,必须在调用 constrained 之前调用: ``` $table->foreignId('user_id') ->nullable() ->constrained(); ``` 要删除一个外键,你需要使用 dropForeign 方法,将要删除的外键约束作为参数传递。外键约束采用的命名方式与索引相同。即,将数据表名称和约束的字段连接起来,再加上 _foreign 后缀: ``` $table->dropForeign('posts_user_id_foreign'); ``` 或者,可以给 dropForeign 方法传递一个数组,该数组包含要删除的外键的列名。数组将根据 Laravel 的 Schema 生成器使用的约束名称约定自动转换: ``` $table->dropForeign(['user_id']); ``` 你可以在迁移文件中使用以下方法来开启或关闭外键约束: ``` Schema::enableForeignKeyConstraints(); Schema::disableForeignKeyConstraints(); ``` 注意:SQLite 默认禁用外键约束。使用 SQLite 时,请确保在数据库配置中启用 启用外键支持 然后再尝试在迁移中创建它们。另外,SQLite 只在创建表时支持外键,并且 在修改表时就不会了。