Laravel 11优化MorphMany关联:无额外查询访问父模型数据
问题背景
我们的应用基于Laravel 11(PHP 8.2、MySQL)开发,使用了morphMany关联。在Block::alt()方法中访问父模型数据时,现有三种实现方式都存在查询冗余或设计欠佳的问题,具体内容如下:
简化代码示例
Post.php
class Post extends BaseModel { public function blocks(): MorphMany { return $this->morphMany(Block::class, 'blockable'); } }
Block.php
class Block extends Model { public function blockable(): MorphTo { return $this->morphTo(); } public function alt(): string { $alt = ''; if ($this->caption) { $alt = $this->caption; } else if ($this->blockable->title) { $alt = 'Block from ' . $this->blockable->title; } return $alt; } }
三种实现方式及问题
- 仅预加载
blocks
$posts = Post::where('status_id', 1) ->with('blocks') ->get();
生成约59条查询语句(大量重复查询)
- 预加载
blocks和blocks.blockable
$posts = Post::where('status_id', 1) ->with('blocks', 'blocks.blockable') ->get();
生成约17条查询语句(有所改善,但存在额外查询)
- 显式传递父模型到
alt方法
$posts = Post::where('status_id', 1) ->with('blocks') ->get(); // 修改后的Block::alt方法 public function alt(Post $blockable): string { $alt = ''; if ($this->caption) { $alt = $this->caption; } else if ($blockable->title) { $alt = 'Block from ' . $blockable->title; } return $alt; }
生成约14条查询语句,但设计上不够优雅
请问是否存在无需额外查询或显式传递父模型的更优实现方案?
更优实现方案
以下是三种实用的解决思路,均能避免额外查询且无需显式传递父模型:
方案一:关联查询时注入父模型字段+访问器生成alt
1. 修改Post模型的blocks关联
在关联中通过联表查询直接将父模型的title注入到Block的属性中:
class Post extends BaseModel { public function blocks(): MorphMany { return $this->morphMany(Block::class, 'blockable') ->selectRaw('blocks.*, posts.title as blockable_title') ->join('posts', function ($join) { $join->on('posts.id', '=', 'blocks.blockable_id') ->where('blocks.blockable_type', '=', Post::class); }); } }
2. 在Block模型中添加访问器
用访问器自动生成alt字段,直接读取注入的blockable_title:
class Block extends Model { protected $appends = ['alt']; public function blockable(): MorphTo { return $this->morphTo(); } public function getAltAttribute(): string { if ($this->caption) { return $this->caption; } return $this->blockable_title ? 'Block from ' . $this->blockable_title : ''; } }
3. 控制器查询
仅需预加载blocks即可,只会生成2条查询语句:
$posts = Post::where('status_id', 1) ->with('blocks') ->get();
方案二:预加载回调动态注入父模型字段(多模型通用)
如果Block关联多种父模型(不止Post),可以用with的回调动态获取对应父模型的字段:
$posts = Post::where('status_id', 1) ->with(['blocks' => function ($query) { $query->addSelect(['blockable_title' => Post::select('title') ->whereColumn('id', 'blocks.blockable_id') ->limit(1)]); }]) ->get();
同样搭配方案一中的Block访问器即可,查询语句数量仍为2条,适配多种关联类型。
方案三:利用setRelation复用已加载的父模型
如果已经预加载了Post和blocks,可在循环中给每个Block绑定已加载的父模型,避免重复查询:
$posts = Post::where('status_id', 1) ->with('blocks') ->get(); foreach ($posts as $post) { foreach ($post->blocks as $block) { $block->setRelation('blockable', $post); } }
此时Block::alt()中访问$this->blockable->title时,会直接使用已加载的模型实例,不会触发新查询,查询语句数量为2条,代码改动最小。
内容的提问来源于stack exchange,提问作者alexmcfarlane
相关产品推荐
相关产品推荐

