一次执行影响分页结果的操作导致的问题

266 阅读1分钟

背景

在一次迁移数据的过程中,Laravel (8.22.1) 的 migration 提示如下错误:

PDOException: SQLSTATE[01000]: Warning: 1265 Data truncated for column 'type' at row 6789

故事的背景是,我们需要把source表中typeA的数据迁移到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();
});

即在迁移完成之后再删除原始数据,从而避免影响分页结果。

此外,如果数据量不大,也可以一次查询出所有数据再处理,也不会出现这样的问题。