Laravel LazyCollection未调用remember()时重复查询问题咨询
本项目包含3个Eloquent模型:
Concert:关联多个Tickets,归属于多个OrdersTicket:包含id、非空字段concert_id、可空字段order_idOrder:非核心关联模型,仅作结构完整性补充
测试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生成器实现,默认不会缓存遍历结果,每次对集合调用遍历、聚合方法时,都会重新执行对应的数据库查询,原代码的执行流程如下:
- 第一次查询:调用
$tickets->count()做票数校验时,触发SQL查询,拿到2条符合条件的可用门票,计数校验通过,不会抛出异常 - 第二次查询:调用
each->reserve()遍历集合预留门票时,再次触发SQL查询,拿到2条可用门票,执行reserve()逻辑把这2条记录的reserved_at字段更新为当前时间并持久化到数据库 - 第三次查询:调用
$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(); } }
异常行为佐证截图:
内容的提问来源于stack exchange,提问作者IGP

