[EagerLoad] leftJoin 和 with 哪个更有用?[N+1 问题]

“任何没写过原始查询语句的人都应该认真考虑一下。”
最近,我的上司和领导们经常跟我说:
(顺便一提,我被安排负责新员工的 MySQL 培训,所以感觉时间非常紧迫。)
N+1 问题
(暂且不谈我自己的故事)
假设建立了以下关系。
class Bolg { public function comments(): HasMany { return $this->hasMany(Comment::class); } }
获取博尔格模型
$blogs = Bolg::query()->first();
此处发出以下查询:
"select * from `blogs`"
通过继续循环,我们可以得到与每个 Blog 模型关联的所有 Comment 模型。
$comments = $blogs->map(function ($blog) { return $blogs->comments->first(); });
当然,由于这是一个循环,以下查询将执行与章节数相同的次数。
"select * from `blogs` where `blogs`.`blog_id` = ? and `blogs`.`blog_id` is not null"
因此,检索所有记录的初始查询会执行一次,再加上总共 N 次查询。
这就是 1+N 问题,或者更准确地说,是 N+1 问题。
问题在于,自然而然地,随着章节数的增加,查询次数也会增加,这无疑是个问题。
加入并避免 N+1
现在我们已经讲到重点了。
如前所述,每次都发出查询效率极低,因此我们使用(左)连接或 with 语句。
(顺便一提,有些情况下每次都检索数据反而更好,所以要谨慎选择。)
这些方法可以避免发送大量查询。
但是,它们各自的作用有所不同。
什么是连接?
`join` 语句执行的是内连接。
它会检索两个表中共有的数据,并排除其他所有数据。
`leftJoin` 执行左外连接。
它返回左表中的所有行以及右表中所有匹配的行。它检索
符合连接条件的数据,同时保留左表中的所有数据。
这些连接是 SQL 的一项特性。
它们可以将多个表合并到一个查询中。
查询语句大致如下所示。
0 => array:3[ "query" => "select * from `blogs` inner join `comments` on `blogs`.`id` = `blog_id`" "bindings" => [] "time" => 1.5 ],
令人惊讶的是,这只需要一次查询。
这真是不可思议的进步!
那么,和……在一起呢?
和
使用 `with` 的查询只需要两步。虽然
比 `join` 慢,但考虑到循环可能运行 N+1 次,这已经是一个巨大的进步了。
顺便一提,`with` 函数的参数字符串是模型文件中关系定义方法的名称。
(它本质上与被关联的模型的名称相同,但要注意,只有当 `with` 被定义为方法时才能使用。)
$blogAndComments= Blog::query() ->with('comments') ->first();
查看查询语句,我们可以看到使用 whereIn 可以一次性检索所有数据。
0 => array:3[ "query" => "select * from `blogs`" "bindings" => [] "time" => 0.5 ], 1 => array:3[ "query" => "select * from `blogs` where `blogs`.`blogt_id` in (?)" "bindings" => [] "time" => 1.5 ],
各自的特点和用途
我们采用两种方法获得了该模型,现在让我们来看看每种方法的特点。
●(左)JOIN 命令
根据连接条件合并模型。JOIN
是一个 SQL 函数,用于一次性从多个表中检索数据。
检索数据时,会创建符合连接条件的数据。
例如,您可以将其理解为创建与 Comment 模型相同数量的最终数据。
这在连接多个表时可能会导致无效数据。
能够一次性获取所有数据当然很有吸引力,
如果您不需要将其用于建模或想要简单的平面数据,那么这种方式就非常理想。
● `with`
模型以关联数组的形式检索,无需合并。
在 ORM 中预加载模型关系来解决 N+1 问题
例如,与 Bolg 模型关联的 Comment 模型会存在于名为 `relations` 的属性下。
由于模型是按照父子关系的层级顺序嵌套检索的,因此它们不会被合并。
相关的模型和数据会一次性检索完毕,避免数据冗余。
当您希望将检索到的数据直接作为模型处理时,这也非常有用。
顺便说一下,可以使用 with 来检索关系,只要它们持续存在,例如 (ModelA.ModelB.ModelC.),但由于嵌套意味着所有传递的模型都将作为外壳包含在内,因此最终需要使用 unset 或类似方法将其删除。
总结与题外话
现在所有信息都已公布,我将对其进行总结,然后在结论之前再补充一些题外话。
概括
你觉得怎么样?
由于信息量很大,我把它整理成了一个表格。

我不知道哪个方法真正有用!
虽然结果大致相同——都是从多个表中检索数据——但使用的方法却截然不同。
因此,结论其实很常见:“根据所需的数据格式更改采集方法。”
最简单的您是想要原始模型还是可以合并的模型确定方法是决定
或者,根据哪种格式更便于后续处理,在嵌套格式和平面格式之间。
此外,根据要检索的数据总量来调整它我认为你应该
会读取关联关系,因此嵌套越深,速度就越慢,相比之下,`join` 就慢得多。
简而言之,这取决于具体情况。
附注(EagerLoad)
我想写写 EagerLoad。
你可能已经了解它了,但简单来说,它的意思就是你不想每次都执行 SQL 查询。
这有点枯燥,所以让我详细解释一下。
“EagerLoad 仅适用于动态属性”这就是它的含义。
(也许这说不通)
首先,我们来谈谈动态属性和关系方法之间的区别。
换句话说,这和
$blog= 新建博客;$blog->评论;
这就是区别所在。
$blog = new Blog; $blog->comments();
`->comments()` 返回一个动态属性,即一个集合。它还
具有“延迟加载”特性,这意味着关系数据仅在被访问时才加载。
相比之下,`->comments()` 返回的是一个关系对象。
你不能将查询构建器与动态属性链接在一起,但
->comments()->where('create_user_id', 1)
你可以像这样将它们链接在一起:
在这种情况下,似乎最好将其作为关系对象获取,对吗?
现在,请记住 EagerLoad 和其他相关方法(包括 `with`)的属性。
是的,你之前已经发出过查询,对吧?
由于你已经检索了数据(在 `relations` 属性中),所以
当你写 `$blog->comment->create_user_id` 时,查询不会运行。
这
意味着“除非是动态属性,否则不能使用 EagerLoad“如果使用 EagerLoad,除非后续处理也使用动态属性,否则毫无意义!!”换句话说,
这是一个自相矛盾的情况。
那么,在检索数据时,如何应用条件、限制和搜索呢?!
$blog = Bolg::with(['comments' => function ($query) { $query->where('create_user_id', 1); }])->first(); $comments = $blog->comments;
这样,您就可以将 EagerLoad 与条件、约束和搜索结合使用。
另外,如果您有一些经常使用的条件,可以将它们转换为模型文件中的方法,这样可以提高代码的可读性,也更方便使用。
结束
我希望这篇文章能让访问世界各地的数据库变得更容易一些。
7
