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

Laravel Eloquent:无外键关联下获取Reservation与Week模型的关联关系

解决Laravel中基于日期范围的Eloquent关联问题

嘿,我来帮你搞定这个问题!首先得明确你当前代码的问题所在:你的weeks()方法直接返回了查询结果(Collection实例),但Eloquent要求关联方法必须返回Relation类的实例(比如HasMany、BelongsTo这类),这样才能支持with()预加载和其他关联特性。再加上你的关联是基于日期范围而非传统的ID外键,所以没法直接用Laravel内置的关联类型,得用自定义关联来实现。

我给你两种方案,按需选择:

方案1:简单版(不支持预加载)

如果你暂时不需要预加载功能,只是想让方法能像关联一样调用(比如$reservation->weeks返回对应Week集合,$reservation->weeks()返回查询构造器),只需要修改你的方法,去掉get()即可:

class Reservation extends Model {
    public function weeks() {
        // 返回Builder实例,而非直接执行查询
        return Week::whereDate('starting_date', '>=', $this->starting_date)
            ->whereDate('starting_date', '<', $this->ending_date)
            ->orderBy('starting_date');
    }
}

这样修改后,$reservation->weeks会自动执行查询返回Week集合,你还能通过$reservation->weeks()->where(...)追加额外查询条件。但缺点是不能用Reservation::with('weeks')->get()预加载,因为Laravel不知道如何批量处理这种日期关联逻辑。

方案2:完整版(支持预加载)

如果需要支持with()预加载,就得创建自定义关联类,让Laravel明白如何批量查询并匹配Reservation和Week的关系。

步骤1:创建自定义关联类

在你的项目中新建app/Relations/HasManyByDateRange.php文件,内容如下:

<?php

namespace App\Relations;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class HasManyByDateRange extends HasMany
{
    protected $localStartField;
    protected $localEndField;
    protected $relatedStartField;

    public function __construct(
        Builder $query,
        Model $parent,
        string $relatedStartField,
        string $localStartField,
        string $localEndField
    ) {
        $this->localStartField = $localStartField;
        $this->localEndField = $localEndField;
        $this->relatedStartField = $relatedStartField;

        // 因为没有传统外键,这里传入空字符串占位,后续会覆盖查询条件
        parent::__construct($query, $parent, '', '');
    }

    /**
     * 为单个模型添加关联约束
     */
    public function addConstraints()
    {
        if (static::$constraints) {
            $startDate = $this->parent->{$this->localStartField};
            $endDate = $this->parent->{$this->localEndField};

            $this->query
                ->whereDate($this->relatedStartField, '>=', $startDate)
                ->whereDate($this->relatedStartField, '<', $endDate);
        }
    }

    /**
     * 为预加载添加批量约束
     */
    public function addEagerConstraints(array $models)
    {
        $reservationIds = collect($models)->pluck($this->parent->getKeyName())->toArray();

        // 通过whereExists关联Reservation和Week的日期范围
        $this->query->whereExists(function ($query) use ($reservationIds) {
            $query->select(\DB::raw(1))
                ->from($this->parent->getTable())
                ->whereColumn('weeks.' . $this->relatedStartField, '>=', $this->parent->getTable() . '.' . $this->localStartField)
                ->whereColumn('weeks.' . $this->relatedStartField, '<', $this->parent->getTable() . '.' . $this->localEndField)
                ->whereIn($this->parent->getTable() . '.' . $this->parent->getKeyName(), $reservationIds);
        });
    }

    /**
     * 初始化模型的关联属性
     */
    public function initRelation(array $models, $relation)
    {
        foreach ($models as $model) {
            $model->setRelation($relation, $this->related->newCollection());
        }

        return $models;
    }

    /**
     * 将预加载的Week结果匹配到对应的Reservation
     */
    public function match(array $models, \Illuminate\Database\Eloquent\Collection $results, $relation)
    {
        // 建立Week到Reservation的映射
        $weekToReservations = collect($models)->flatMap(function ($reservation) use ($results) {
            return $results->filter(function ($week) use ($reservation) {
                return $week->{$this->relatedStartField} >= $reservation->{$this->localStartField}
                    && $week->{$this->relatedStartField} < $reservation->{$this->localEndField};
            })->map(function ($week) use ($reservation) {
                return [$reservation->getKey() => $week];
            });
        })->groupBy(0)->map(function ($items) {
            return $items->pluck(1);
        });

        // 为每个Reservation设置关联的Week集合
        foreach ($models as $model) {
            if (isset($weekToReservations[$model->getKey()])) {
                $model->setRelation($relation, $weekToReservations[$model->getKey()]);
            }
        }

        return $models;
    }
}

步骤2:在Reservation模型中使用自定义关联

修改你的Reservation模型,引入自定义关联类,并更新weeks()方法:

<?php

namespace App\Models;

use App\Relations\HasManyByDateRange;
use Illuminate\Database\Eloquent\Model;

class Reservation extends Model
{
    // ... 其他模型代码(比如$fillable等)

    public function weeks()
    {
        return new HasManyByDateRange(
            $this->newQueryForRelation(Week::class),
            $this,
            'starting_date', // Week表中的起始日期字段名
            'starting_date', // Reservation表中的起始日期字段名
            'ending_date'    // Reservation表中的结束日期字段名
        );
    }
}

这样修改后,你就可以正常使用Reservation::with('weeks')->get()来预加载每个Reservation对应的Week集合了,同时也支持$reservation->weeks直接获取集合、$reservation->weeks()->where(...)追加查询条件等所有Eloquent关联的常用操作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 07:32:37