🍟

LaravelのwhereIn実装時のtips

2023/05/19に公開

結論

whereInの対象をCollectionにして、chunkとflatMapを用いると少ない手間でデータ量が増大した時の対応も可能。
オリジナル

$ids = $this->getIds();
$items = DB::table('items')->whereIn('id', $ids)->get();

whereInの対応(従来)

$ids = $this->getIds();
$items = collect();
foreach ($array_chunk($ids, 1000) as $chunkIds) {
    $items = $items->concat(DB::table('items')->whereIn('id', $chunkIds)->get());
}

whereInの対応(提案)

$ids = $this->getIds();
$items = collect($ids)->chunk(1000)->flatMap(function ($chunkIds) {  
    return DB::table('items')->whereIn('id', $chunkIds)->get();  
});

説明

実装でwhereの条件にin句を使うことがよくあります。

select
    *
from
    items
where
    items.id in (1, 2, 3);

Laravelだとクエリビルダーを用いて以下のように記述することができます。

DB::table('items')->whereIn('id', [1, 2, 3]);

whereInの第二引数は、変数にして与えることにします。

$ids = [1, 2, 3];
DB::table('items')->whereIn('id', $ids);

こうすることで、以下のように変動する値をin句に設定することができます。

$ids = $this->getIds();
$items = DB::table('items')->whereIn('id', $ids)->get();

結果はCollectionとして取得することにしておきます。
すごく便利ですが、in句の要素数が増えるとパフォーマンスに影響が出てしまいます。
Oracleでは、1000件という上限が設定されているようです。
その他のDBに関しては設定されていないものの、分割する方がパフォーマンスが出るという点では共通していると思います。
今回は、とりあえず1000件ずつに分割する方法について考えていきたいと思います。

方針を考えます。

  1. $idsという要素数が不定のものを一定数ずつに分割する。
  2. $itemsという結果を入れるためのCollectionを用意する。
  3. 分割された$ids でクエリを実行する。
  4. 結果を$itemsに追加する。
  5. 分割された$idsがなくなるまで処理を行う。
    こんな感じかと思います。

これをコードで書いていきます。

  1. $idsという要素数が不定のものを一定数ずつに分割する。

これに関しては、array_chunkが使えそうです。

$ids = $this->getIds();
$chunkedIds = array_chunk($ids, 1000);
  1. $itemsという結果を入れるためのCollectionを用意する。
  2. 分割された$ids でクエリを実行する。
  3. 結果を$itemsに追加する。
  4. 分割された$idsがなくなるまで処理を行う。

これらに関しては、ループして代入する処理になるかと思うのでまとめて記述します。

// 略
$items = collect();
foreach ($chunkedIds as $chunkIds) {
    $items = $items->concat(DB::table('items')->whereIn('id', $chunkIds)->get());
}

このような形になるかと思います。

合わせると以下のようになります。

$ids = $this->getIds();
$chunkedIds = array_chunk($ids, 1000);
$items = collect();
foreach ($chunkedIds as $chunkIds) {
    $items = $items->concat(DB::table('items')->whereIn('id', $chunkIds)->get());
}

$chunkedIdsをインライン化できそうなので修正しましょう。

$ids = $this->getIds();
$items = collect();
foreach (array_chunk($ids, 1000) as $chunkIds) {
    $items = $items->concat(DB::table('items')->whereIn('id', $chunkIds)->get());
}

これで、めでたく完成です。

どうでしょうか?
コード量はそんなにありませんが、個人的にループ文のせいでコードの見通しが少し悪くなっているような気がします。

この辺りは好みの問題もあるのであまり触れませんが、せっかく便利なCollectionというものがあるので、それを用いて少しコードをスマートにしてみたいと思います。

まず、$idsをCollectionにしましょう。

$ids = $this->getIds();
collect($ids);

これで、コレクションのメソッドを使うことができます。
chunkというメソッドを使うことで分割できそうです。

$ids = $this->getIds();
collect($ids)->chunk(1000);

これで、分割ができます。
次に分割されたコレクションに対して、それぞれに処理をするメソッドを考えていきます。
最初に考えられるのはeachかと思います。

$ids = $this->getIds();
$items = collect();
collect($ids)->chunk(1000)->each(function ($chunkIds) use ($items) {
    $items = $items->concat(DB::table('items')->whereIn('id', $chunkIds)->get());
});

このような感じになると思います。
ただ、これでは先ほどとあまり変わっていないように感じる上、動作しません。
eachの中で$itemsを更新することはできないため、別の方法を検討する必要があります。
この場合であれば、一つの値を返すreduceが使えそうです。

$ids = $this->getIds();
$items = collect($ids)->chunk(1000)->reduce(function ($curry, $chunkIds) {
    return $curry->concat(DB::table('items')->whereIn('id', $chunkIds)->get());
}, collect());

これで動作するようになり、だいぶスッキリしたと思います。
自分的にも、これでいいかなというか、こんな感じの実装しか思いつかなかったのですが、
確認がてら上記の内容をChatGPT(3.5)に聞いてみたら、このようなものが返されました。

$ids = $this->getIds();
$items = collect($ids)->chunk(1000)->flatMap(function ($chunkIds) {
    return DB::table('items')->whereIn('id', $chunkIds)->get();
});

一瞬、ん?となったのですが、読んでみるとなるほどこういう書き方もできるのかと理解できました。
mapでそのまま追加して、後で平坦化する。
言われてみれば理解できるのですが、自分では全く思いつかなかったです。
直感的ではないので好みが分かれるのかもしれないですが、記述量が減るので覚えておいて損はないなと思いました。
個人的には好みなので今後使って行きたいなと思います。

記事用に簡略化したものなので、実際に検証はできていません。
実装のイメージとして受け取ってもらえれば幸いです。

ご指摘等あれば、ぜひコメントお願いします。

株式会社THIRD エンジニアブログ

Discussion