Laravel中使用whereHas时如何向关联函数传递参数
Laravel中向关联函数传递参数的问题及解决方案
问题描述
在Laravel开发中,我定义了带参数的模型关联方法(非查询回调):
- UserRelationships模型包含接收Carbon日期参数的
scopeAt动态作用域; - User模型的
progenial_relation、parental_relation关联方法支持可选Carbon日期参数,直接调用这些关联并传递参数时可正常工作。
但使用whereHas、has这类预加载查询方法时,尝试多种语法均无法向关联函数传递参数。若在查询回调中重复调用scopeAt,生成的SQL会出现重复的日期约束,进而使查询失效。现咨询:是否存在可行的方式向关联函数传递参数?或有什么替代方案?
相关代码示例
UserRelationships模型
class UserRelationships extends Model { public function scopeAt($query, Carbon $date) { return $query->where('superseded_at', '>', $date ) ->where('created_at', '<=', $date ); } }
User模型
class User extends Authenticatable { public function progenial_relation(Carbon $date=null) { return $this->hasOne(UserRelationships::class, 'user_id', 'id') ->at( @$date ?: Carbon::now() ) ->orderByDesc('created_at') ->limit(1); } public function parental_relation(Carbon $date=null) { return $this->hasMany(UserRelationships::class, 'sponsor_id', 'id') ->at( @$date ?: Carbon::now() ); } // 尝试的无效写法 public function children(Carbon $date=null) { $date = @$date ?: Carbon::now(); return self::whereHas('progenial_relation', function($q) { $q->where('sponsor_id', $this->id); }, $date); // 不生效 } // 导致重复约束的写法 public function children(Carbon $date=null) { $date = @$date ?: Carbon::now(); return self::whereHas('progenial_relation', function($q) use ($date) { $q->at($date)->where('sponsor_id', $this->id); }); } }
生成的错误SQL
select * from `users` where exists ( select * from `user_relationships` where `users`.`id` = `user_relationships`.`user_id` and `user_relationships`.`superseded_at` > ? and `user_relationships`.`created_at` <= ? and `sponsor_id` = ? and `user_relationships`.`superseded_at` > ? and `user_relationships`.`created_at` <= ? ) and `users`.`deleted_at` is null
解决方案
方案1:拆分基础关联与带参数关联(推荐)
将关联关系与日期约束拆分,基础关联仅定义关联规则,带参数的关联方法在基础关联上添加约束,whereHas调用基础关联并在回调中添加日期条件:
class User extends Authenticatable { // 基础关联:仅定义关联关系,不带任何约束 public function progenial_relation_base() { return $this->hasOne(UserRelationships::class, 'user_id', 'id') ->orderByDesc('created_at') ->limit(1); } public function parental_relation_base() { return $this->hasMany(UserRelationships::class, 'sponsor_id', 'id'); } // 保留原有的带参数关联方法,供直接调用使用 public function progenial_relation(Carbon $date=null) { return $this->progenial_relation_base()->at($date ?: Carbon::now()); } public function parental_relation(Carbon $date=null) { return $this->parental_relation_base()->at($date ?: Carbon::now()); } // 正确的children查询方法 public function children(Carbon $date=null) { $date = $date ?: Carbon::now(); return self::whereHas('progenial_relation_base', function($q) use ($date) { $q->at($date)->where('sponsor_id', $this->id); }); } }
这种方式逻辑清晰,避免了重复添加约束的问题,同时保留了原关联方法的调用习惯。
方案2:自定义查询范围封装逻辑
在User模型中定义查询范围,将关联查询与日期约束的逻辑封装在一起:
class User extends Authenticatable { // ... 保留原关联方法 ... public function scopeHasChildrenAt($query, Carbon $date=null) { $date = $date ?: Carbon::now(); return $query->whereHas('progenial_relation_base', function($q) use ($date) { $q->at($date)->where('sponsor_id', $this->id); }); } } // 使用示例 $user->hasChildrenAt($specificDate)->get();
方案3:利用闭包传递参数(Laravel 8+)
Laravel 8及以上版本支持通过闭包形式定义关联并传递参数,但写法相对繁琐:
class User extends Authenticatable { public function progenial_relation(Carbon $date=null) { $date = $date ?: Carbon::now(); return $this->hasOne(UserRelationships::class, 'user_id', 'id') ->at($date) ->orderByDesc('created_at') ->limit(1); } public function children(Carbon $date=null) { $date = $date ?: Carbon::now(); return self::whereHas(function($query) use ($date) { // 将关联的约束添加到查询中 $this->progenial_relation($date)->addConstraintsToQuery($query); }, function($q) { $q->where('sponsor_id', $this->id); }); } }
原因分析
原写法中,whereHas会先调用progenial_relation关联方法,该方法已通过at($date)添加了日期约束;之后在回调中再次调用at($date),导致SQL中重复生成相同的日期条件,最终引发查询失效。
内容的提问来源于stack exchange,提问作者Valentino
相关产品推荐
相关产品推荐

