Laravel迁移中SQLite环境修改主键的非冗余实现方案
问题说明
- 最初创建
items表的迁移使用UUID类型的id作为主键,代码如下:
Schema::create('items', function(Blueprint $table) { $table->uuid('id')->primary(); // 其余字段定义... });
- 后续迭代需要新增自增主键列,一开始写的结构调整迁移代码如下:
Schema::table('items', function(Blueprint $table) { $table->dropPrimary('id'); $table->rename('id', 'SystemId')->change(); $table->id(); });
- 执行迁移时直接失败:SQLite不支持直接修改表主键。官方文档给出的兼容方案是删除原表后按新结构重建,但这种写法需要把首次迁移里的全量表结构代码复制一遍,完全违反DRY原则,需要更优雅的实现路径。
可行实现方案
SQLite底层不支持主键修改的限制是绕不开重建步骤的,但完全没必要硬编码复制所有表结构,两种可落地的方案都能避免重复代码:
方案一:动态读取表结构完成重建,零硬编码重复字段
通过Schema门面提供的字段读取能力,自动获取原表的字段、属性,动态生成新表结构,不需要手动写任何原有字段的定义:
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; return new class extends Migration { public function up() { $originTable = 'items'; $tempTable = 'items_temp'; // 创建符合新结构的临时表 Schema::create($tempTable, function (Blueprint $table) use ($originTable) { // 先定义调整后的主键结构 $table->id(); $table->uuid('SystemId')->unique(); // 自动读取原表所有字段,跳过原id字段,其余字段自动按原结构生成 $originColumns = Schema::getColumnListing($originTable); foreach ($originColumns as $column) { if ($column === 'id') continue; $columnType = DB::getSchemaBuilder()->getColumnType($originTable, $column); $table->{$columnType}($column)->nullable(); } }); // 把原表数据迁移到临时表,原id值映射到SystemId字段 $dataColumns = array_filter(Schema::getColumnListing($originTable), fn($col) => $col !== 'id'); DB::statement("INSERT INTO $tempTable (SystemId, " . implode(',', $dataColumns) . ") SELECT id AS SystemId, " . implode(',', $dataColumns) . " FROM $originTable"); // 替换原表 Schema::drop($originTable); Schema::rename($tempTable, $originTable); } public function down() { // 回滚逻辑反向操作即可 $originTable = 'items'; $tempTable = 'items_temp'; Schema::create($tempTable, function (Blueprint $table) use ($originTable) { $table->uuid('id')->primary(); $originColumns = Schema::getColumnListing($originTable); foreach ($originColumns as $column) { if (in_array($column, ['id', 'SystemId'])) continue; $columnType = DB::getSchemaBuilder()->getColumnType($originTable, $column); $table->{$columnType}($column)->nullable(); } }); $dataColumns = array_filter(Schema::getColumnListing($originTable), fn($col) => !in_array($col, ['id', 'SystemId'])); DB::statement("INSERT INTO $tempTable (id, " . implode(',', $dataColumns) . ") SELECT SystemId AS id, " . implode(',', $dataColumns) . " FROM $originTable"); Schema::drop($originTable); Schema::rename($tempTable, $originTable); } };
如果表存在自定义索引、外键约束,读取字段时同步拉取索引、外键定义,在临时表创建时自动加上即可,全程不需要手动维护原有结构代码。
方案二:安装doctrine/dbal依赖,让Laravel自动处理重建
Laravel的SQLite驱动本身内置了不兼容DDL的自动兼容逻辑:遇到SQLite不支持的表修改操作时,框架会自动完成临时表创建、数据迁移、表替换的全流程,只需要安装依赖后调整迁移的执行顺序即可,不需要自己写重建逻辑:
- 先安装必要依赖:
composer require doctrine/dbal - 调整迁移代码,拆分操作步骤避免主键冲突:
// 第一步:先加临时字段存储新主键值 Schema::table('items', function(Blueprint $table) { $table->unsignedBigInteger('new_id')->nullable(); }); // 第二步:给新主键填充自增初始值 DB::unprepared('SET @row_num = 0'); DB::unprepared('UPDATE items SET new_id = (@row_num := @row_num + 1)'); // 第三步:执行字段重命名、主键调整,框架会自动处理SQLite兼容 Schema::table('items', function(Blueprint $table) { $table->renameColumn('id', 'SystemId'); $table->renameColumn('new_id', 'id'); $table->primary('id'); $table->bigIncrements('id')->change(); });
注意事项
- 执行迁移前务必备份数据库,避免操作失误导致数据丢失
- 如果表数据量超过百万级,重建表的锁表时间会比较长,建议在业务低峰期执行
- 存在外键关联的表,迁移完成后记得同步更新关联表的外键类型和关联约束
内容的提问来源于stack exchange,提问作者shaedrich
相关产品推荐
相关产品推荐

