[转发]在Laravel6需要操作大量内存的地方使用LazyCollection

Laravel框架
395
0
0
2022-10-16
标签   Laravel基础
Laravel团队在新版本Laravel6中添加了许多新特性,本文主要讨论LazyCollection(惰性集合包装器)
在Laravel中,Illuminate\Support\Collection类提供了一个非常方便的包装器来处理数据对象的数组。实际上,所有的Eloquent的数据库查询总是作为Collection实例来返回,当然不包括查询具体某一条数据的情况,相信使用Laravel的朋友对Collection类的操作都已经很熟悉了。LazyCollection本质上扩展了Collection类的功能。

什么是LazyCollection?

LazyCollection类和Illuminate\Support\Collection类相似只是加了点糖,该类基本上使用了php的生成器特性使你可以处理非常大的数据集,同时保持较低的内存占用,如果你不熟悉生成器,请参考鸟哥的一篇文章,非常简单易懂。
生成器类似于PHP中的普通函数,使用yield关键字代替return。它的行为与return相似,因为它向函数的调用者返回一个值,但不是将函数从堆栈中删除,这使函数可以从再次调用时的位置继续。​​因此,无论哪个只要包含了“yield”的函数都是生成器,就像“实时”从函数返回值一样,您无需在函数本身中维护值的状态。一旦没有更多的值要产生,则生成器可以简单地退出,并且调用代码将继续进行,就像数组的值用完了一样。
LazyCollection利用生成器的这种行为,以便在处理较大数据集时保持较低的内存占用。在这里,我们可以使用LazyCollection实例与传统Collection类方法结合使用,例如解析一个很大的日志文件。在这里,可以使用惰性集合而不是一次性将整个文件读取到内存中,而是仅将文件的一小部分保留在内存中。
use App\LogEntry;
use Illuminate\Support\LazyCollection;

LazyCollection::make(function () {
    $handle = fopen('log.txt', 'r');

    while (($line = fgets($handle)) !== false) {
        yield $line;
    }
})->chunk(4)->map(function ($lines) {
    return LogEntry::fromLines($lines);
})->each(function (LogEntry $logEntry) {
    // 具体操作。。。
});
请注意,我们传递给该匿名函数make的是一个生成器函数,因为使用了yield的关键字(并将返回Generator一个可以使用foreach循环进行迭代的实例对象)。在经过chunk方法进行分割,并最终在each方法中处理已经切割好的数据行。

在模型中使用LazyCollection

我们可以在自定义模型实例上使用查询对象的“cursor”方法来来返回一个LazyCollection实例,让我们检查一下cursor方法的实现。

public function cursor()
{
    if (is_null($this->columns)) {
        $this->columns = ['*'];
    }

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

如您所见,返回了一个LazyCollection实例,我们依然能够像操作Collection实例一样来操作它。我们仍然只对数据库运行一个查询,不同的是一次只在内存中加载了一个用户模型实例。以下面为例。

$users = App\User::cursor()->filter(function ($user) {
    return $user->id > 500;
});

foreach ($users as $user) {
    echo $user->id;
}

如您所见,在此示例中,filter方法只有在我们实际逐个遍历每个用户之前,才执行回调,从而大大减少了内存使用。

来源 zhuanlan.zhihu.com/p/90607027