0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravelのchunkメソッドで全レコードが更新されなかった話

Last updated at Posted at 2024-11-02

概要

chunkは一度にクエリの結果の小さな塊(チャンク)を取得し、各チャンクをクロージャーに送り込んで処理します。
注意点としてはチャンクのクロージャー内でレコードの更新や削除を行う場合、主キーや外部キーの変更がチャンクのクエリ結果に影響する可能性があります。

chunkとは 💡

公式 doc

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の注意点

公式doc

チャンクのクロージャー内でレコードの更新や削除を行う場合、主キーや外部キーの変更がチャンクのクエリ結果に影響する可能性があります。
チャンクされた結果にレコードが含まれなくなり意図した処理ではなくなることがあります。

今回はこれに該当する問題を起こしてしまっていました。。

意図した処理ができなかった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メソッドを使う

公式doc

このメソッドは、レコードの主キーに基づいて自動的に結果をページ分割します。

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の際にクロージャ内でレコードを更新する際には気をつけてという意味が身にしめてわかりました

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?