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

Laravel中用预加载替代多查询的优化方案咨询

问题场景与现有代码

我正在学习提升Laravel使用技巧,现有**客户(Customer)、预约(Visit)、用户(User)**模型:用户为客户创建预约,客户可拥有多个预约。

Visit模型代码

class Visit extends Model
{
    use HasFactory;

    protected $fillable = [
        'user_id',
        'customer_id',
        'report',
        'appointment_date',
        'appointment_time',
    ];

    protected $dates = ['appointment_date'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }
}

Customer模型代码

class Customer extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'city',
        'address',
        'email_address',
        'phone_number',
    ];
}

现有判断脚本

我编写了脚本判断客户是否需创建新预约(条件:无未来预约,且最近预约超指定月数或无预约),脚本如下:

$customers = Customer::get();
foreach ($customers as $customer) {
    $Newvisit = $customer->visits()->whereInFuture()->first();
    // continue if the customer already has an appointment planned in the future
    if ($Newvisit !== null) {
        continue;
    }

    // This also retrieves visits with null appointment date, this is neccesary for appointments that are not schedulded but already planned
    $lastVisit = $customer->visits()->OrderBy('appointment_date', 'desc')->first();
    // Customer has no visits yet
    if ($lastVisit === null) {
        $this->createNewAppointment($customer);
        continue;
    }

    $lastVisitDate = new Carbon($lastVisit->appointment_date);
    // Last appointment for customer is longer than .. months ago
    if ($lastVisitDate->diffInMonths() > $this->argument('months')) {
        $this->createNewAppointment($customer);
        continue;
    }
}

疑问

请问能否通过预加载(eager loading)将上述逻辑优化为仅执行一次查询?因需获取未来预约和最近预约,是否无法实现该优化?


优化方案

当然可以优化,而且能彻底避免循环中多次查询数据库的问题,下面提供几种高效的实现方式:

方法一:子查询整合判断条件(仅1次查询)

直接在客户查询中通过子查询计算每个客户的「是否有未来预约」「最近预约日期」,一次查询就能拿到所有判断所需的数据:

$months = $this->argument('months');
$today = Carbon::today();

$customers = Customer::query()
    ->selectRaw('customers.*')
    // 子查询:判断当前客户是否有未来预约
    ->selectRaw('EXISTS(SELECT 1 FROM visits WHERE visits.customer_id = customers.id AND visits.appointment_date >= ?) AS has_future_visit', [$today])
    // 子查询:获取当前客户最近的预约日期(含null的未排期预约)
    ->selectRaw('(SELECT appointment_date FROM visits WHERE visits.customer_id = customers.id ORDER BY appointment_date DESC LIMIT 1) AS last_visit_date')
    ->get();

foreach ($customers as $customer) {
    if ($customer->has_future_visit) {
        continue;
    }

    // 无任何预约 或 最近预约超出指定月数
    if (is_null($customer->last_visit_date) || Carbon::parse($customer->last_visit_date)->diffInMonths($today) > $months) {
        $this->createNewAppointment($customer);
    }
}

方法二:直接筛选符合条件的客户(无需循环判断)

如果你的目标只是找到需要创建新预约的客户,可以直接在查询中完成筛选,不用遍历所有客户:

$months = $this->argument('months');
$today = Carbon::today();
$cutoffDate = $today->subMonths($months);

$targetCustomers = Customer::query()
    // 排除有未来预约的客户
    ->whereDoesntHave('visits', function ($query) use ($today) {
        $query->where('appointment_date', '>=', $today);
    })
    // 筛选:无任何预约 或 最近预约早于 cutoffDate
    ->where(function ($query) use ($cutoffDate) {
        $query->doesntHave('visits')
            ->orWhereHas('visits', function ($subQuery) use ($cutoffDate) {
                $subQuery->where('appointment_date', '<=', $cutoffDate)
                    ->whereRaw('appointment_date = (SELECT MAX(appointment_date) FROM visits WHERE customer_id = customers.id)');
            });
    })
    ->get();

// 直接处理目标客户
foreach ($targetCustomers as $customer) {
    $this->createNewAppointment($customer);
}

关于预加载的补充说明

你担心的「需要获取未来和最近预约无法预加载」其实是可以实现的,但效率不如子查询:
先在Customer模型中定义lastVisit关联:

public function lastVisit(): HasOne
{
    return $this->hasOne(Visit::class)->latest('appointment_date');
}

然后用预加载获取数据:

$customers = Customer::with([
    'visits' => function ($query) use ($today) {
        $query->where('appointment_date', '>=', $today)->limit(1);
    },
    'lastVisit'
])->get();

这种方式会产生3次查询(1次查客户,2次查关联),虽然比原来的N+2次查询好,但还是不如子查询的1次高效,所以优先推荐前两种子查询方案。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 14:17:51