Laravel 6.xのLazy Collection(新機能)を紹介します

さてさて、先日Laravelのバージョン6.xがリリースされたので前回記事では(実装方法が変更になっていた)ログイン機能の使い方をご紹介しました。

そして、新バージョンのLaravelには変更だけでなく追加機能もいくつか用意されていて中でも開発者が「ゲーム・チェンジャー」と呼ぶ機能があります。

それが、

Lazy Collection(れいじーこれくしょん)

です。

これは、これまでのLaravelでも人気が高いCollectionの機能を拡張するもので、使い方によってはコードの実行を省力化することができる機能になっています。

ということで、今回はLaravel 6.xの新機能の中からLazy Collectionを紹介します。ぜひ皆さんのお役に立てると嬉しいです😊✨

開発環境: Laravel 6.1

Lazy Collectionとは

まず、Lazy Collectionがどんなものかというと「必要なときに必要なものだけ処理する」コレクションで、これによってどんないいことが起こるかというとPHPのメモリ使用量を減らすことができるのです。

例えば、DBから100万件もあるような大量のデータを取得するとします。通常だと、まず100万件のデータをメモリ上に保存して、そこから必要なデータをとりだすという流れになるわけですが、Lazy Collectionの場合は必要なデータのみのメモリ量でOKになります。

ちょっと違うかもしれませんが、必要な時に必要なだけ処理をするという意味では画面が移動した時にだけ画像を読み込む「Lazy loading」という機能にアイデアは似ているかもしれません。

Lazy Collectionの仕組み

Lazy Collectionは、Laravel独自のものではなくPHPGeneratorsという機能を利用したものになっています。

例えば、Generatorsは以下のように「1〜100万の範囲内で1から2ずつ進めた数字を表示する」という例を見てみましょう。

foreach(range(1, 1000000, 2) as $number) {

    echo "$number "; // ここは、1、3、5・・・となる

}

この場合はrange()はまず1 〜 100万までの数字をメモリ上に用意します。そして、その後で2ずつ進めた数字をピックアップする形になります。

当然先に全ての数字を準備するので、メモリもその全てに必要になってきます。

では、Generatorsを使った場合を見てみましょう。

function xrange($start, $limit, $step = 1) {

    for($i = $start; $i <= $limit; $i += $step) {

        yield $i; // ここで必要なデータを返す

    }

}

foreach(xrange(1, 1000000, 2) as $number) {

    echo "$number ";

}

まず、xrange()というのが独自につくったGeneratorsで、この中で必要なデータだけyieldで返すことになります。これを実行すると先ほど紹介したrange()の例とまったく同じ結果になりますが、メモリの使用量は必要な分だけでよくなります。

では、メモリの最大利用値が分かるmemory_get_peak_usage()でこの2つコードを実行して計測するとどうなるでしょうか。

私の環境では、結果は以下のようになりました。

  • range() ・・・ 19,556,472(約 19.5 MB)
  • xrange() ・・・ 2,799,800(約 2.7 MB)

なんとGeneratorsを使うとメモリは7分の1程度で実行できました。

つまり、Lazy CollectionはこのGeneratorsを利用した「メモリを省力化したコレクション」というわけですね。

Lazy Collectionの使い方

大きく分けて2パターンありますのでひとつずつ紹介していきます。

DBを使わない場合

例えば、以下のサンプルのような1行ずつ名前が記述されたテキストファイルがあるとします。

(サンプルのテキストファイル)

山田太郎
鈴木次郎
田中三郎
(以下続く...)

そして、この中から名前の中に「田」が入っている人だけをLazy Collectionとして取得するには以下のようにします。

$collection = LazyCollection::make(function() {

    $handle = fopen('name.txt', 'r');

    while(($line = fgets($handle)) !== false) {

        if(Str::contains($line, '田')) {

            yield trim($line);

        }

    }

});

ちなみにこのコードで必要なネームスペースは以下の2つです。

use Illuminate\Support\LazyCollection;
use Illuminate\Support\Str;

こうすることで、全員の名前をメモリに用意しなくても通常のコレクションと同じ使い方が出来るというわけです。例えば、全員に「さん」をつける場合はこのようになります。

$collection = LazyCollection::make(function() {

    // 省略

})->map(function($name){

    return $name .' さん';

});

dump($collection->toArray());

結果はこうなります。

DBを使う場合

続いてはデータベースを使う場合です。
例えば前回の記事で作った以下のテストユーザーで試してみましょう。

では、この中から「メールアドレスに “s” が入っているデータ」だけ取得してみます。

$users = \App\User::cursor()->filter(function($user) {

    return Str::contains($user->email, 's');

});

dd($users->toArray());

重要な部分は、今回は先ほどのようにyieldを使っていない点です。
これは、cursor()メソッドの中ですでに定義されているからです。

(Laravelのコードからの引用)

return new LazyCollection(function () {
    yield from $this->connection->cursor(
        $this->toSql(), $this->getBindings(), ! $this->useWritePdo
    );
});

Lazy Collectionのメソッド

eager()

Laravel 6.1から追加されたeager()メソッドを使えばcursor()を使ったSQL実行を「省エネ化」することができます。

例えば、以下のような場合を見てみましょう。

\DB::listen(function($q) {

    dump($q->sql); // ここで実行されたSQL文を表示する

});

$users = \App\User::cursor();

$count = $users->count();
$user_array = $users->toArray();
$user_json = $users->toJson();

実は、これを実行すると同じSQL文が3回も実行されてしまいます。

これを1回で済ませるために用意されたのがeager()です。
count()toArray()toJson()が呼ばれる前に以下のようにします。

$users = \App\User::cursor();
$users = $users->eager(); // ←ここを追加

$count = $users->count();
$user_array = $users->toArray();
$user_json = $users->toJson();

※ このメソッドは、2019.10.03に追記しました。

tapEach()

tapEach()を使えば、Lazy Collectionが必要なデータのみコールバック関数を実行させることが出来ます。

まず、通常のeach()メソッドを使った場合は、以下のように回数分すべてが実行される異なります。(times()はその回数分だけ実行するメソッドです)

LazyCollection::times(10)->each(function($value){

    // ここは、10回呼ばれる

})
->take(3)
->all();

それが、tapEach()を使うと、必要なデータ3つ分だけ実行されることになります。

LazyCollection::times(10)->tapEach(function($value) {

    // ここは、3回だけ呼ばれる

})
->take(3) // ここの回数分 tapEach() を実行することになる
->all();

ちなみに – 1

ちなみに、Lazy Collectionはコレクションの構造自体が変化する以下のようなメソッドは使えないようです。

  • shift
  • pop
  • prepend

ちなみに – 2

英語ですが、LaracastsこちらのページLazy Collectionの動画が公開されています。現在、無料で見られますので興味のある方はぜひご覧になってください。

おわりに

ということで、今回はLaravel 6.xの新機能Lazy Collectionを紹介しました。

それほどデータ量が多くないサイトだとあまり恩恵は少ないかもしれませんが、何年も利用してデータが大量にあるようなサイトでは使い方によっては大幅にメモリ使用量を減らすことができると思います。

なお、今回Lazy Collectionを勉強するにあたって初めてGeneratorsという機能を知ることになりました。それほど大量のデータを扱う案件に携わっていなかったことに加えて最近のコンピュータはとても高性能になったことが原因でしょうが、こんな便利な機能があるとは知りませんでした。

やっぱりプログラムの奥は深いですね。
これからも精進していきたいと思います。

ではでは〜!

この記事が役立ちましたらシェアお願いします😊✨