假设,我有以下简化的对象模型:
/**
* @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 个独立的实例。
我该如何解决?