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

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;
  }
}

三种实现方式及问题

  1. 仅预加载blocks
$posts = Post::where('status_id', 1)
  ->with('blocks')
  ->get();

生成约59条查询语句(大量重复查询)

  1. 预加载blocks和blocks.blockable
$posts = Post::where('status_id', 1)
  ->with('blocks', 'blocks.blockable')
  ->get();

生成约17条查询语句(有所改善,但存在额外查询)

  1. 显式传递父模型到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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 13:39:51