Laravel6 Lazy Collection

今回はlaravel6に新しく追加された機能

Lazy Collectionについて勉強しました。

まず

今までも存在していたCollectionクラス

/Users/xxxxxx/Code/laravel6/vendor/laravel/framework/src/Illuminate/Support/Collection.php

はarrayのラッパークラスだったが

今回新たに追加されたLazyCollectionクラス

/Users/xxxxxx/Code/laravel6/vendor/laravel/framework/src/Illuminate/Support/LazyCollection.php

はiteratorをラップしていて、メモリの使用料をかなり減らせることが可能になります。

初期設定

画像1

現在はUsersテーブルに435687行のデータが入っている状態です。

問題

この状態ですべてのデータを取得してくると

web.php

<?php
Route::get('/', function () {
   $users = \App\User::all();
   return "Done";
});

どうなるか?

Symfony\Component\Debug\Exception\FatalErrorException
Allowed memory size of 134217728 bytes exhausted (tried to allocate 56309768 bytes)
http://laravel6.test/

やはりメモリーのリミットに達してしまいエラーになります。

これは取得してきたコレクションをすべてメモリに積もうとしてしまうためです。

Generator

ここで先ほどのユーザー取得のall()をcursor()に変更します。

<?php
Route::get('/', function () {
   $users = \App\User::cursor();
   dump($users);
   return "Done";
});

これでgeneratorをリターンしてきます。

LazyCollection {#252 ▼
 +source: Closure() {#253 ▼
   class: "Illuminate\Support\LazyCollection"
   this: LazyCollection {#249 …}
   use: {▶}
   file: "/Users/xxxxxx/Code/laravel6/vendor/laravel/framework/src/Illuminate/Support/LazyCollection.php"
   line: "601 to 605"
 }
}
Done

この時点ではクエリーはまだ実行されていません。

下記のように、一番初めの行を取得しようと指示を出した時に初めてクエリが実行されます。

<?php
Route::get('/', function () {
   $users = \App\User::cursor();
   dump($users->first());
   return "Done";
});

結果

User {#262 ▼
 #fillable: array:3 [▶]
 #hidden: array:2 [▶]
 #casts: array:1 [▶]
 #connection: "mysql"
 #table: "users"
 #primaryKey: "id"
 #keyType: "int"
 +incrementing: true
 #with: []
 #withCount: []
 #perPage: 15
 +exists: true
 +wasRecentlyCreated: false
 #attributes: array:8 [▶]
 #original: array:8 [▶]
 #changes: []
 #dates: []
 #dateFormat: null
 #appends: []
 #dispatchesEvents: []
 #observables: []
 #relations: []
 #touches: []
 +timestamps: true
 #visible: []
 #guarded: array:1 [▶]
 #rememberTokenName: "remember_token"
}
Done

ここで確認のために実際にcursor()に時にクエリが実行されていないことを確かめたいと思います。

<?php
Route::get('/', function () {
   DB::listen(function($query){
       dump($query->sql);
   });
   $users = \App\User::cursor();
   // dump($users->first());
   return "Done";
});

これを実行してもクエリは出力されません。

しかし、コメントアウトしていた部分も実行させると

<?php
Route::get('/', function () {
   DB::listen(function($query){
       dump($query->sql);
   });
   $users = \App\User::cursor();
   dump($users->first());
   return "Done";
});
"select * from `users`"
User {#268 ▼
 #fillable: array:3 [▶]
 #hidden: array:2 [▶]
 #casts: array:1 [▶]
 #connection: "mysql"
 #table: "users"
 #primaryKey: "id"
 #keyType: "int"
 +incrementing: true
 #with: []
 #withCount: []
 #perPage: 15
 +exists: true
 +wasRecentlyCreated: false
 #attributes: array:8 [▶]
 #original: array:8 [▶]
 #changes: []
 #dates: []
 #dateFormat: null
 #appends: []
 #dispatchesEvents: []
 #observables: []
 #relations: []
 #touches: []
 +timestamps: true
 #visible: []
 #guarded: array:1 [▶]
 #rememberTokenName: "remember_token"
}
Done

この実行結果からもわかるように、ここで初めてクエリが実行され、メモリにもすべてのコレクションが入らないということがわかります。

応用

今度はLazy Collection を利用し、ログファイルに文字列を書きます。

Route::get('/', function () {
   $users = \App\User::cursor();
   foreach($users->take(500) as $user){
       logger($user->name);
   }
   return "Done";
});

これを実行すると

例) storage/logs/laravel-2019-10-20.log に出力されてます

今度はこのログデータをブラウザに表示させます。

<?php
function readLogFile($file){
   $fp = fopen($file, 'r');
   while($line = fgets($fp)){
       yield $line;
   }
}
Route::get('/', function () {
   foreach(readLogFile(storage_path('logs/laravel-2019-10-20.log')) as $line){
       dump($line);
   }
   return "Done";
});

LazyCollectionを使うと

<?php
use Illuminate\Support\LazyCollection;
Route::get('/', function () {
   $lines = new LazyCollection(function(){
       $fp = fopen(storage_path('logs/laravel-2019-10-20.log'), 'r');
       while($line = fgets($fp)){
           yield $line;
       }
   });
   dd($lines->map(function($line){
       return strlen($line);
   })->all());
   return "Done";
});

LazyCollection::makeを使うと

<?php
use Illuminate\Support\LazyCollection;
Route::get('/', function () {
   dd(LazyCollection::make(function(){
       $fp = fopen(storage_path('logs/laravel-2019-10-20.log'), 'r');
       while($line = fgets($fp)){
           yield $line;
       }
   })
   ->map(function($line){
       return strlen($line);
   })
   ->all());
   return "Done";
});


アメリカで働いたあと、日本に帰ってきて今はフリーランスで働いています。最近はLaravelを使った案件が多いのでLaravelなどについての情報を共有できたらと考えています。