LaravelのwhereIn実装時のtips
結論
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件ずつに分割する方法について考えていきたいと思います。
方針を考えます。
-
$ids
という要素数が不定のものを一定数ずつに分割する。 -
$items
という結果を入れるためのCollectionを用意する。 - 分割された
$ids
でクエリを実行する。 - 結果を
$items
に追加する。 - 分割された
$ids
がなくなるまで処理を行う。
こんな感じかと思います。
これをコードで書いていきます。
$ids
という要素数が不定のものを一定数ずつに分割する。
これに関しては、array_chunk
が使えそうです。
$ids = $this->getIds();
$chunkedIds = array_chunk($ids, 1000);
$items
という結果を入れるためのCollectionを用意する。- 分割された
$ids
でクエリを実行する。- 結果を
$items
に追加する。- 分割された
$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
でそのまま追加して、後で平坦化する。
言われてみれば理解できるのですが、自分では全く思いつかなかったです。
直感的ではないので好みが分かれるのかもしれないですが、記述量が減るので覚えておいて損はないなと思いました。
個人的には好みなので今後使って行きたいなと思います。
記事用に簡略化したものなので、実際に検証はできていません。
実装のイメージとして受け取ってもらえれば幸いです。
ご指摘等あれば、ぜひコメントお願いします。
Discussion