You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel LazyCollection未调用remember()时重复查询问题咨询

问题背景

本项目包含3个Eloquent模型:

  • Concert:关联多个Tickets,归属于多个Orders
  • Ticket:包含id、非空字段concert_id、可空字段order_id
  • Order:非核心关联模型,仅作结构完整性补充

测试LazyCollection特性时,为Concert类实现reserveTickets门票预留方法遇到异常:改用普通Collection可正常运行,但使用LazyCollection时,执行$reservedTickets->all()无法得到预期的2张已预留门票,需要明确该问题的产生原理。

复现场景

复现代码如下:

// 创建1个演唱会记录
$concert = Concert::factory()->create();
// 为该演唱会创建3张关联门票
Ticket::factory()->for($concert)->count(3)->create();
// 预留2张门票(设置reserved_at时间戳)并返回预留的门票
$reservedTickets = $concert->reserveTickets(quantity: 2);

// 此处返回结果不包含预期的2张已预留门票
$reservedTickets->all();
问题代码

reserveTickets方法依赖同模型内定义的findTickets方法,原实现代码如下:

public function reserveTickets(int $quantity): LazyCollection
{
    return $this->findTickets($quantity)->each->reserve();
}

public function findTickets(int $quantity): LazyCollection
{
    $tickets = $this->tickets()->available()->limit($quantity)->cursor();

    throw_if(exception: NotEnoughTicketsException::class, condition: $quantity > $tickets->count());

    return $tickets;
}
临时修复方案

测试发现,如果将findTickets中的查询逻辑修改为在cursor()后链式调用remember()方法,功能即可正常运行:

$tickets = $this->tickets()->available()->limit($quantity)->cursor()->remember();

此前误以为访问LazyCollection内容时会抛出自定义NotEnoughTicketsException,实测并不会,加remember()后功能可正常运行。未调用remember()时,对应的SELECT查询会累计执行3次,导致throw_if票数不足校验逻辑多数情况下失效。

问题原理

核心原因是cursor()返回的LazyCollection底层依赖PHP生成器实现,默认不会缓存遍历结果,每次对集合调用遍历、聚合方法时,都会重新执行对应的数据库查询,原代码的执行流程如下:

  1. 第一次查询:调用$tickets->count()做票数校验时,触发SQL查询,拿到2条符合条件的可用门票,计数校验通过,不会抛出异常
  2. 第二次查询:调用each->reserve()遍历集合预留门票时,再次触发SQL查询,拿到2条可用门票,执行reserve()逻辑把这2条记录的reserved_at字段更新为当前时间并持久化到数据库
  3. 第三次查询:调用$reservedTickets->all()获取最终结果时,第三次触发SQL查询,此时之前的2张门票已经被设置了reserved_at值,不再满足available()作用域中whereNull('reserved_at')的筛选条件,查询返回空集合,自然无法拿到预期的2张预留门票。

调用remember()方法后,LazyCollection会将第一次遍历生成的结果缓存到内存,后续所有操作都复用缓存的模型实例,不会重复执行SQL查询:第一次count校验时查询拿到2张门票并缓存,后续的预留操作、all()结果获取都直接使用缓存的2个模型实例,不会重复查库,因此可以返回正确的已预留门票。

相关模型完整实现

Concert模型

<?php

namespace App\Models;

use App\Exceptions\NotEnoughTicketsException;
use Illuminate\Database\Eloquent;
use Illuminate\Support\{Collection, LazyCollection};

class Concert extends Eloquent\Model
{
    use Eloquent\Factories\HasFactory;

    protected $guarded = [];

    /* 关联定义 */
    public function tickets(): Eloquent\Relations\HasMany
    {
        return $this->hasMany(Ticket::class);
    }

    public function orders(): Eloquent\Relations\BelongsToMany
    {
        return $this->belongsToMany(Order::class, 'tickets');
    }

    /* 业务方法 */
    public function orderTickets(string $email, int $ticket_quantity): Order
    {
        $tickets = $this->findTickets($ticket_quantity);

        return $this->createOrder($email, $tickets);
    }

    public function reserveTickets(int $quantity): LazyCollection
    {
        return $this->findTickets($quantity)->each->reserve();
    }

    public function findTickets(int $quantity): LazyCollection
    {
        $tickets = $this->tickets()->available()->limit($quantity)->cursor();

        throw_if(exception: NotEnoughTicketsException::class, condition: $quantity > $tickets->count());

        return $tickets;
    }
}

Ticket模型

<?php

namespace App\Models;

use Illuminate\Database\Eloquent;

class Ticket extends Eloquent\Model
{
    use Eloquent\Factories\HasFactory;

    protected $guarded = [];

    /* 查询作用域 */
    public function scopeAvailable(Eloquent\Builder $query): void
    {
        $query->whereNull('order_id')->whereNull('reserved_at');
    }

    /* 业务方法 */
    public function reserve(): void
    {
        $this->fill(['reserved_at' => now()])->save();
    }
}

异常行为佐证截图:
Tinker调试会话展示上述异常行为


内容的提问来源于stack exchange,提问作者IGP

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 19:39:12