Laravelのchunkは便利だけど、クエリ条件を更新するような処理の場合に挙動がおかしくなり、切ないことになったので、そう言った時の代替案を考えてみた。
どんな問題が起きるかについては、すでに書いている方もいるが、一応書いておく。
前提
- タスクというデータを一括で完了にしたい
- 完了はis_completedが1の状態の時
Chunkで起きる問題
is_completedが0のレコードを取得するような条件にしていて、クロージャー内でis_completedを更新すると更新分の取得件数が除外されてしまい、処理がずれてしまう。
不具合の起きるコード
// 未完了のタスクを3件ずつ取得して完了にする
Task::where('is_completed', 0)->chunk(3, function ($tasks) {
foreach ($tasks as $task) {
// 完了タスクに更新
$task->is_completed = 1;
$task->save();
}
});
起きる不具合のイメージ
- task1:最初のループで処理される
- task2:最初のループで処理される
- task3:最初のループで処理される
- task4:1〜3が更新されたことによって、indexがずれ、2回めのループ対象にならない
- task5:1〜3が更新されたことによって、indexがずれ、2回めのループ対象にならない
- task6:1〜3が更新されたことによって、indexがずれ、2回めのループ対象にならない
- task7:2回めのループ対象になり更新される。
- task8:2回めのループ対象になり更新される。
- task9:2回めのループ対象になり更新される。
task1〜3がクエリ条件から消えたので、task4〜6のindexがtask1〜3に昇格してしまった感じ。
代替案
limit()->get()->isEmpty() + whileにするだけ。これだけで対象のレコードに対して全件処理できる。
// 未完了のタスクを3件ずつ取得して完了にする
while(!($tasks = Tasks::where('is_completed', 0)->limit(3)->get())->isEmpty()) {
foreach ($tasks as $task) {
// 完了タスクに更新
$task->is_completed = 1;
$task->save();
}
}
まとめ
- 事前に対象のレコードをチェックせずにすむので、データ量を意識せずに解決できる
- whileはこういう処理が向いている。
備考
- 変数の初期化とか$loop=trueみたいなことをしても解決できるけど、めんどくさかった。
- ただ単にwhileの条件や変数定義をワンライナーにしたかっただけ。