背景
在一次迁移数据的过程中,Laravel (8.22.1) 的 migration 提示如下错误:
PDOException: SQLSTATE[01000]: Warning: 1265 Data truncated for column 'type' at row 6789
故事的背景是,我们需要把source表中type为A的数据迁移到target表中,并删除source.type列的枚举值A,上述错误就是在删除A的时候发生的。
原因
删除A的语句如下:
ALTER TABLE `source` MODIFY COLUMN `type` ENUM('B','C') NOT NULL
当source中还存在使用A的数据时,删除该枚举值就会提示上述错误。
显然,数据迁移出现了遗漏。
程序如下:
DB::transaction(function (): void {
DB::table('source')->where('type', 'A')->orderBy('id')->each(function (array $source_row): void {
// Insert into target table using $source_row
// ...
DB::table('source')->where('id', $source_row['id'])->delete();
});
});
为了避免占用大量内存,这里使用了Illuminate\Database\Concerns\BuildsQueries::each()方法对数据逐次进行处理,并在迁移之后删除该条数据。
each()方法的源码如下:
public function each(callable $callback, $count = 1000)
{
return $this->chunk($count, function ($results) use ($callback) {
foreach ($results as $key => $value) {
if ($callback($value, $key) === false) {
return false;
}
}
});
}
所以,它通过chunk()方法对数据库做分页查询,并逐页执行回调函数。
分页查询的 SQL 语句如下:
select * from `source` where `type` = 'A' order by `id` asc limit 1000 offset 0
因此,当我们在回调函数中迁移并删除掉 1000 条数据后,下次将从再次查询后的第 1001 条开始取出一页数据进行处理,这就造成该次查询的前 1000 条数据被遗漏。
解决
我们对代码做如下调整:
DB::transaction(function (): void {
$source_ids = [];
DB::table('source')->where('type', 'A')->orderBy('id')->each(function (array $source_row) use (&$source_ids): void {
// Insert into target table using $source_row
// ...
$source_ids[] = $source_row['id'];
});
DB::table('source')->whereIn('id', $source_ids)->delete();
});
即在迁移完成之后再删除原始数据,从而避免影响分页结果。
此外,如果数据量不大,也可以一次查询出所有数据再处理,也不会出现这样的问题。