九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、先日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
独自のものではなくPHP
のGenerators
という機能を利用したものになっています。
例えば、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
という機能を知ることになりました。それほど大量のデータを扱う案件に携わっていなかったことに加えて最近のコンピュータはとても高性能になったことが原因でしょうが、こんな便利な機能があるとは知りませんでした。
やっぱりプログラムの奥は深いですね。
これからも精進していきたいと思います。
ではでは〜!