Laravel Eloquent:无外键关联下获取Reservation与Week模型的关联关系
嘿,我来帮你搞定这个问题!首先得明确你当前代码的问题所在:你的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

