TES Blog

株式会社テクニカルエンジニアリングサポートに所属する社員が、自身が携わるテクノロジーやイベントに関する情報を発信しています。

Eloquent の chunk でも skip がしたい!

はじめに

TES に入社して6年目となる、橋本に勤務する橋本です。

最近は Laravel で開発するのが楽しくて、会社でも広まってきて嬉しいです。

今回は、Laravel に同梱されている Eloquent ORM の chunk メソッドで skip(offset)を指定する方法を紹介します。

前提

Laravel のバージョン

php artisan -V
Laravel Framework 6.1.0

そもそも

「え? chunk で skip って指定できないの??」

って思うかもしれませんが、できません。

以下のようなコードを実行した時に、

<?php

Book::skip(2)->chunk(2, function ($books) {
    // 処理
});

期待する SQL はこうですが、

select * from `books` order by `books`.`id` asc limit 2 offset 2; -- skip で指定した 2 が最初から設定されている
select * from `books` order by `books`.`id` asc limit 2 offset 4;

実際には skip(2) で指定した offset 値は無視されてしまいます🥺

select * from `books` order by `books`.`id` asc limit 2 offset 0; -- skip で指定した 2 が無視されている
select * from `books` order by `books`.`id` asc limit 2 offset 2;

じゃあどうするのか

私はサービスプロバイダで Builder のマクロを定義し、以下のような実装にしました。

※ マクロを定義するやり方はググればいくらでもでます。

<?php

/**
  * 指定の件数をスキップした状態から chunk 処理を実行する
  *
  * @param int $limit 分割数
  * @param int $offset スキップしたい件数
  * @param callable 分割単位で処理したいクロージャ
  * @return boolean
  */
Builder::macro("chunkWithSkip", function ($limit, $offset, callable $callback) {
    do {
        $results = $this->skip($offset)->take($limit)->get();
        $resultsCount = $results->count();

        // 結果がなければ処理を終了する
        if (empty($resultsCount)) {
            break;
        }

        // 元の chunk の実装と同じように false が返ってきたら処理を中断する
        if ($callback($results) === false) {
            return false;
        }

        $offset += $resultsCount;
    } while ($resultsCount == $limit);

    return true;
});

このようにして直感的に使えます!

<?php

Book::chunkWithSkip(2, 2, function ($books) {
    // 処理
});
select * from `books` order by `books`.`id` asc limit 2 offset 2;
select * from `books` order by `books`.`id` asc limit 2 offset 4;

こちらの Issue を参考にさせていただきました🙏🏻

github.com

別のアプローチ

skip 対象となる id の最大値を取得して where でそれ以上の条件データに絞って chunk するやり方です。

where で絞ってから chunk した方がパフォーマンスが良いですし、 Eloquent の chunk メソッドが利用できます。

<?php

// chunk の開始位置となる id を取得
$id = Book::skip(2)->take(1)->value('id');

// 取得した id 以上のデータを条件に chunk する
Book::where('id', '>=', $id)->chunk(2, function ($books) {
    // 処理
});
select * from `books` where `id` >= 3 order by `books`.`id` asc limit 2 offset 0;

さいごに

そもそものそもそもなんですが、こんな実装しないといけないの設計か仕様がダメなんじゃないの…?
というわけで、実際のプロジェクトでは仕様変更となり、今回紹介した実装は使用しませんでした笑

こういう関数ほしい…と思った時にマクロで気軽に実装できるのはとても良いですね!

まだまだ Laravel 駆け出しですが、これからも使っていきたいと思います。