chunkとは 💡
chunkメソッドは、一度にクエリの結果の小さな塊(チャンク)を取得し、各チャンクをクロージャーに送り込んで処理します。
何千ものデータベース・レコードを扱う必要がある場合に有効です。
例)usersテーブル全体を一度に100レコードのチャンクで取得
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
DB::table('users')->orderBy('id')->chunk(100, function (Collection $users) {
foreach ($users as $user) {
// ...
}
});
chunkの注意点
チャンクのクロージャー内でレコードの更新や削除を行う場合、主キーや外部キーの変更がチャンクのクエリ結果に影響する可能性があります。
チャンクされた結果にレコードが含まれなくなり意図した処理ではなくなることがあります。
今回はこれに該当する問題を起こしてしまっていました。。
意図した処理ができなかったchunk 🚨
$query = Employee::query()->where('position','Developer');
$query->chunk(10, function ($employees) {
foreach ($employees as $employee) {
$this->info('Processing employee: ' . $employee->id);
$employee->position = 'Manager';
$employee->save();
}
});
クエリの説明
employeesテーブルのpositionカラムがDeveloperのレコードを取得
chunk内のクロージャーの説明
positionカラムをManagerに更新
意図していること
全件のemployeesテーブルのpositionカラムがDeveloperのレコードをManagerに更新する
結果
123レコード中、63レコードしか更新されなかった
ログでレコードのidを見る
以下のidの更新がスキップされているように見えます。
- 11~20
- 30~41
- 51~60
- 71~80
- 91~100
- 111~120
Processing employee: 1
Processing employee: 2
Processing employee: 3
Processing employee: 4
Processing employee: 5
Processing employee: 6
Processing employee: 7
Processing employee: 8
Processing employee: 9
Processing employee: 10
Processing employee: 21
Processing employee: 22
Processing employee: 23
Processing employee: 24
Processing employee: 25
Processing employee: 26
Processing employee: 27
Processing employee: 28
Processing employee: 29
Processing employee: 30
Processing employee: 41
Processing employee: 42
Processing employee: 43
Processing employee: 44
Processing employee: 45
Processing employee: 46
Processing employee: 47
Processing employee: 48
Processing employee: 49
Processing employee: 50
Processing employee: 61
Processing employee: 62
Processing employee: 63
Processing employee: 64
Processing employee: 65
Processing employee: 66
Processing employee: 67
Processing employee: 68
Processing employee: 69
Processing employee: 70
Processing employee: 81
Processing employee: 82
Processing employee: 83
Processing employee: 84
Processing employee: 85
Processing employee: 86
Processing employee: 87
Processing employee: 88
Processing employee: 89
Processing employee: 90
Processing employee: 101
Processing employee: 102
Processing employee: 103
Processing employee: 104
Processing employee: 105
Processing employee: 106
Processing employee: 107
Processing employee: 108
Processing employee: 109
Processing employee: 110
Processing employee: 121
Processing employee: 122
Processing employee: 123
なぜレコードがスキップされたのか ❓
chunkで実行されるクエリを見る
select * from `employees`
where `position` = ? order by `employees`.`id` asc limit 10 offset 0 ["Developer"]
↑これが10件発行されます。
select * from `employees`
where `position` = ? order by `employees`.`id` asc limit 10 offset 10 ["Developer"]
↑次にこれが10件発行されます。
と続いていきます。
わかったこと
1回目のチャンクでカラム値を更新すると、次のチャンクのクエリ結果では
idが11~20が先頭のレコードとして取得されます。
しかし、これは2回目のチャンクの為offset10によりスキップされます。
よって2回目ではidが21~30のレコードがクエリで取得されクロージャー内で更新されます。
これをくり返しています。
以下イメージです。
id | カラム値(取得時) | クロージャー内で更新されたカラム値 | チャンク番号 |
---|---|---|---|
1 | Developer | Manager | 1 |
2 | Developer | Manager | 1 |
3 | Developer | Manager | 1 |
4 | Developer | Manager | 1 |
5 | Developer | Manager | 1 |
6 | Developer | Manager | 1 |
7 | Developer | Manager | 1 |
8 | Developer | Manager | 1 |
9 | Developer | Manager | 1 |
10 | Developer | Manager | 1 |
11 | Developer | ← offset10によりスキップ | - |
12 | Developer | ← offset10によりスキップ | - |
13 | Developer | ← offset10によりスキップ | - |
14 | Developer | ← offset10によりスキップ | - |
15 | Developer | ← offset10によりスキップ | - |
16 | Developer | ← offset10によりスキップ | - |
17 | Developer | ← offset10によりスキップ | - |
18 | Developer | ← offset10によりスキップ | - |
19 | Developer | ← offset10によりスキップ | - |
20 | Developer | ← offset10によりスキップ | - |
21 | Developer | Manager | 2 |
22 | Developer | Manager | 2 |
23 | Developer | Manager | 2 |
24 | Developer | Manager | 2 |
25 | Developer | Manager | 2 |
26 | Developer | Manager | 2 |
27 | Developer | Manager | 2 |
28 | Developer | Manager | 2 |
29 | Developer | Manager | 2 |
30 | Developer | Manager | 2 |
どうすれば良いのか 💡
orderBy('id')
でprimarykeyを指定して取得する
$query = Employee::query()->orderBy('id')->where('position','Developer');
$query->chunkByID(10, function ($employees) use ($query) {
foreach ($employees as $employee) {
$this->info($query->toSql()); // SQL文を表示
$this->info(json_encode($query->getBindings()));
$this->info('Processing employee: ' . $employee->id);
$employee->position = 'Manager';
$employee->save();
}
});
sqlは以下のようになっています
select * from `employees` where `position` = ? order by `id` asc ["Developer"]
chunkByIdメソッドを使う
このメソッドは、レコードの主キーに基づいて自動的に結果をページ分割します。
DB::table('users')->where('active', false)
->chunkById(100, function (Collection $users) {
foreach ($users as $user) {
DB::table('users')
->where('id', $user->id)
->update(['active' => true]);
}
});
sqlは以下のようになっています
select * from `employees` where `position` = ? ["Developer"]
参考 ✨
最後に
chunkの際にクロージャ内でレコードを更新する際には気をつけてという意味が身にしめてわかりました