Laravel8 Eloquent动态关联方法无法访问模型属性问题
现有Activity模型,需要根据自身type字段值返回不同的关联模型,实现动态关联,初始模型代码如下:
class Activity extends Model { protected $fillable = [/*....a bunch of fields....*/]; protected $with = ['user', 'company', 'resource']; public function company(): BelongsTo { return $this->belongsTo(Company::class); } public function user(): BelongsTo { return $this->belongsTo(User::class, 'generated_by'); } /** * @throws \Exception */ public function resource(): HasOne { if ($this->type === '1') { return $this->hasOne(Document::class, 'id', 'document_id'); } else if ($this->type === '2') { return $this->hasOne(Workflow::class, 'id', 'workflow_id'); } else { throw new \Exception("err"); } } }
最初控制器通过如下代码获取活动数据:
$activities = $request->user()->company->activities;
运行时出现异常:执行resource()关联方法时,当前模型实例的$attributes数组为空,无法读取type属性值。
将resource从$with全局预加载数组移除后,通过遍历手动访问关联属性可以正常运行:
$activities = $request->user()->company->activities; foreach ($activities as $event) { $event->resource; // 存在加载副作用,会自动将关联结果挂载为模型属性 // $event->resource()->get(); 无法正常将resource挂载为模型属性 }
核心疑问:
- 是否存在方案可以保证关联方法执行时模型属性已完成加载,支持直接在
resource()方法中访问$this->type而不返回null? - 当前这种动态关联的实现方式是否存在设计误区?
Laravel的关联预加载运行逻辑是:先新建一个空的当前模型实例,调用关联方法拿到关联关系配置(包括关联表名、外键、本地键等),再根据配置批量查询关联数据。这个空模型实例不会填充任何数据库字段值,因此在关联方法内直接访问$this->type时拿到的就是null。
手动遍历访问$event->resource时,每个$event已经是从数据库查询完成、属性全部填充完毕的实例,因此这时候调用关联方法可以正常读取type值。
方案1:保留自定义动态关联,调整预加载时机
不要把resource放到全局$with数组中,先查询主模型数据,待属性填充完成后再触发关联加载:
// 先查询主模型,此时所有Activity实例的属性已从数据库填充完成 $activities = $request->user()->company->activities; // 再加载关联,此时调用resource()方法时可以正常读取type属性 $activities->load('resource');
如果需要全局自动加载该关联,可以在模型的booted方法中注册retrieved事件,模型从数据库查询完成后自动加载关联,无需每次手动调用load:
protected static function booted() { static::retrieved(function ($model) { $model->load('resource'); }); }
注意:该实现存在N+1查询问题。框架无法提前批量区分不同type的Activity分组查询关联数据,只能逐个模型执行关联查询,数据量较大时性能很差。
方案2:使用Laravel原生多态关联(推荐)
当前实现的动态关联本质就是多态关联,Laravel已经提供了标准的内置实现,完全符合框架约定,不会出现预加载时空模型调用的问题,还支持自动批量预加载避免N+1。
调整步骤:
- 给
activities表新增两个多态关联字段:resource_id(存储关联模型的主键ID)、resource_type(存储关联模型的完整类名,如App\Models\Document、App\Models\Workflow) - 将原有的
resource()方法替换为MorphTo关联定义:
public function resource(): MorphTo { return $this->morphTo(); }
- 原有
type字段可以保留做业务标识,也可以直接通过resource_type判断关联类型,原document_id、workflow_id字段可废弃。 - 调整完成后可以直接将
resource放入$with数组做全局预加载,框架会自动按关联类型分组批量查询对应模型数据,无N+1问题。
最初的自定义动态关联实现确实存在设计层面的问题:
- 不符合Laravel关联方法的设计约定:关联方法的核心作用是定义两个模型的关联关系配置,框架会在预加载、关联查询等多个场景下用空模型实例调用关联方法获取配置,关联方法本身不应该依赖模型实例的属性值做分支判断。
- 自定义条件分支的动态关联无法利用框架内置的预加载优化,很容易产生N+1查询问题,后续维护成本高。
- 新增关联类型时需要修改关联方法的条件判断逻辑,不符合开闭原则。
内容的提问来源于stack exchange,提问作者Christian

