MUGIJIRU.JP

Webエンジニアの雑談ブログ

Seederでレコードをループしながら色々やるときの話

LaravelのSeederの小ネタ。

データの変換やら何やらで、特定のテーブルの行をループしながら処理をするという場面はたくさんあると思います。
・レコード数が多いと全行getではメモリが爆発するのでページングする機会も多いはず。
・なんか実行中に進捗やら何やら出したいときもあるはず。

そんな時の書き方の例を紹介します。

<?php
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Builder;

class SampleSeeder extends Seeder
{
    // 実行
    public function run()
    {
        $this->command->info('[!]レコードをループします');

        // データを取得するためのbuilder
        $builder = \App\Shouhin::query();

        // 当バッチ専用の絞り込み条件とか
        $scope = function(Builder $builder) {
            return $builder->withTrashed();
        };

        // 進捗バーの生成
        $progress_bar = $this->command
            ->getOutput()
            ->createProgressBar($this->pagerCount($builder, $scope));

        // ページング
        $page = 1;

        while(($rows = $this->pager($page, $builder, $scope))->count()) {
            // 行ごとのループ処理
            foreach ($rows as $row) {
                // 何かやるならここで。

                // 進捗バー
                $progress_bar->advance();
            }

            $page ++;
        }

        // 進捗バー終了
        $progress_bar->finish();
        $this->command->line(' done.');

        $this->command->info('[!]レコードのループが終わりました');
    }

    /**
     * ページング処理
     *
     * @param integer $page       ページ(1始まり)
     * @param object  $builder    Builder
     * @param object  $scope      第一引数にbuilderを取るクロージャ(where句の追加等)
     * @param integer $per_page   1ページあたりのレコード数
     *
     * @return Collection ページごとのレコード群
     */
    public function pager($page, Builder $builder, Closure $scope = null, $per_page = 5000)
    {
        return $this->addPagerScope($builder, $scope)
            ->skip(($page - 1) * $per_page)
            ->take($per_page)
            ->get();
    }

    /**
     * 行数カウント処理
     *
     * @param object $builder Builder
     * @param object $scope   第一引数にbuilderを取るクロージャ(where句の追加等)
     *
     * @return integer 件数
     */
    public function pagerCount(Builder $builder, Closure $scope = null)
    {
        return $this->addPagerScope($builder, $scope)->count();
    }

    /**
     * builderにスコープを当てる
     *
     * @param object $builder Builder
     * @param object $scope   第一引数にbuilderを取るクロージャ(where句の追加等)
     *
     * @return scope適用後のBuilder
     */
    public function addPagerScope(Builder $builder, Closure $scope = null)
    {
        if ($scope !== null) {
            $scope($builder);
        }

        return $builder;
    }
}

で、実行するとこんな感じ

$ php artisan db:seed --class=SampleSeeder
[!]レコードをループします
 1518/1518 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100% done.
[!]レコードのループが終わりました
$ 

応用していって、builder, scope および一行ごとの処理を差し替えるような構造にすれば
制御に関する行がぐっと減ります。
Modelやら何やらごとの処理を集中してサクサク作っていけるのではないかと思います。