首页 > 解决方案 > 如何避免 Laravel Eloquent 对具有双向对象关联的同一实体重复数据库获取?

问题描述

假设,我有以下简化的对象模型:

/**
 * @property int        id
 * @property bool       is_foo
 * @property Collection posts
 */
class Topic extends Model {
  public function posts(): HasMany {
    return $this->hasMany(Post::class);
  }
}

/**
 * @property int    id
 * @property Topic  topic
 * @property string bar
 */
class Post extends Model {
  public function topic(): BelongsTo {
    return $this->belongsTo(Topic::class);
  }
  
  public function getBarAttribute(?string $bar): ?string {
    return $this->topic->is_foo ? $bar : null;
  }
}

即,一个主题收集了几个帖子。每个帖子都属于一个主题。主题可以或不能是foo-ish,并且每个帖子都有一个属性bar。如果帖子的父主题是 foo-ish,则帖子永远不会返回该bar属性的值。

现在,让我们考虑以下代码片段

$topic = Topic::query()->with('posts')->where('id', '=', 42);
do($topic);

function do(Topic $topic): void {
  foreach($topic->posts as $post) {
    $bar = $post->bar;
    // do something with bar
  }
}

不幸的是,如果 n 是主题 42 中的帖子数,此代码会触发 n+2 个数据库查询。数据库查询是

SELECT * FROM topics WHERE id = 42;
SELECT * FROM posts WHERE topic_id = 42;
SELECT * FROM topics WHERE id = 42;
// ...
// repeated n times
// ...
SELECT * FROM topics WHERE id = 42;

首先,按预期获取主题,然后进行一次获取以急切地加载相关帖子。不幸的是,每个帖子都会再次获取公共父主题,因为Post::getBarAttribute调用Post::topic和 Eloquent 解析了属于关系,而没有注意到父主题已经被获取。此外,Eloquent 弄乱了对象图,Laravel 调试栏告诉我 2n+1 个模型已被水合。我希望 Eloquent 能够水合 n+1 个模型(n 个帖子,1 个主题),并且我希望每个模型Post:topic都指向Topic内存中的同一个实例。但是,Eloquent 会Topic为同一实体创建 n+1 个独立的实例。

我该如何解决?

标签: eloquentrelationshipobject-graphhydration

解决方案


推荐阅读