为什么Laravel/Eloquent不能使用JOIN来进行Eager Loading?

32
<?php

class Cat extends Eloquent {

    public function user() {
        return $this->belongsTo('User');
    }
}

class User extends Eloquent {

    public function cats() {
        return $this->hasMany('Cat');
    }
}

现在:

$cats = Cat::with('user')->get();

执行2个查询:

select * from `cats`
select * from `users` where `users`.`id` in ('1', '2', 'x')
为什么不能只是这样做呢:

select * from cats inner join users on cats.user_id = users.id

对于那些说表中有两个id列的人,可以通过使用别名轻松避免这种情况:

select 
    c.id as cats__id,
    c.name as cats__name,
    c.user_id as cats__user_id,
    b.id as users__id,
    b.name as users__name
from cats c
inner join users b on b.id = c.user_id

更新:

有人指出Eloquent不能从模型中了解表格的列,但我想他们可以提供一种定义模型中列的方法,这样它就可以使用别名并执行适当的联接而不是额外的查询。


2
@deczo 嗯...因为这是一个查询而不是两个?Cat上的user_id不可为空,每个Cat都属于一个User...那么为什么不使用inner join呢? - empz
2
@deczo 再次强调,如果 ORM 设计师希望解决这个问题,这并不是一个主要问题。对象的定义可以轻松地区分“一直存在”和“有时存在”,或者检索“一定存在”和“可能存在”,并相应地选择内部、左、或右连接。我不了解 Eloquent,所以也许他们只是决定保持简单。 - IMSoP
4
我不太明白你的意思。连接语句存在是为了让你无需额外查询就能匹配不同表中的行。如果你在谈论不同类型的连接,比如左连接、右连接、内连接等,我想你可以根据模型中的关系告诉Eloquent要使用哪一种。belongsTo表示它总是属于某个表的,所以外键不能为空。他们可以再添加一个名为canBelongTo的选项,允许外键为空,在这种情况下将使用左连接。不知道,只是随便说说... - empz
1
事实是ORM只是另一层,使生活更加轻松。它的主要目的是让开发人员更容易使用,我认为这个ORM就像它能够达到的那样容易。它有缺陷和限制,存在性能问题,代码也存在不一致性,但另一方面,它真的很雄辩,非常适合简单的任务。然而,没有一个ORM足够灵活,能够满足所有的要求,这就是为什么你(我)不使用ORM处理更复杂的任务。 - Jarek Tkaczyk
2
@emzero,最近我开始研究Laravel,我面临着完全相同的问题,我无法相信一个开发者会说额外的查询是可以接受的,而使用join就可以完成工作和/或select *也可以。 - dav
显示剩余11条评论
3个回答

13

我猜这个功能可以让我们急切地加载多个一对多的关系。比如说,我们还有一个 dogs 表:

class User extends Eloquent {

    public function cats() {
        return $this->hasMany('Cat');
    }

    public function dogs() {
        return $this->hasMany('Dog');
    }
}

现在我们想要使用 User 进行预加载:

$users = User::with('cats','dogs')->get();

没有任何连接可以将这些合并为单个查询。 但是,对于每个“with”元素进行单独的查询是可行的:

select * from `users`
select * from `cats` where `user`.`id` in ('1', '2', 'x')
select * from `dogs` where `user`.`id` in ('1', '2', 'x') 

因此,尽管在某些简单情况下这种方法可能会产生额外的查询,但它提供了一种能够加载更复杂数据的急切加载能力,而连接方法则会失败。

这是我对为什么会这样的猜测。


1
这对于N<>M和1<>N关系是正确的,但不适用于1<>1或N<>1。即使您只实现了这两个关系中的一个,仍然会有很大的改进,特别是对于大型数据集。 (此外,无论集合中的记录数如何,1<>N也可以通过额外的一个查询来解决) - Yaron U.
对于 belongsTo 关系,此规则不适用。 - dav
1
但是Eloquent可以使用join来处理belongsTo关系,而使用另一种查询方式来处理hasMany关系。 - fico7489
1
没有可用于将它们合并为一个查询的连接。但是有一个方法,那就是左连接。 - Adam

4

我认为,当你想要使用LIMIT和/或OFFSET时,连接查询方法存在致命缺陷。

$users = User::with('cats')->get() - 这将输出以下2个查询。

select * from `users`
select * from `cats` where `user`.`id` in ('1', '2', 'x')

而且它不是一个单一的查询

select * from users inner join cats on cats.user_id = users.id

假设我们需要对这个记录集进行分页。

User::with('cats')->paginate(10) - 这将输出以下两个带有限制的查询。

select * from `users` limit 10
select * from `cats` where `user`.`id` in ('1', '2', 'x')

使用join操作,它会变成这样:

select * from users inner join cats on cats.user_id = users.id limit 10

它将获取10条记录,但并不意味着有10个用户,因为每个用户可以拥有多只猫。

另一个原因是,关系型数据库和NOSQL数据库之间的关系可以通过分离的查询方法轻松实现。

正如前面的答案所述,id是模糊的,您必须在每个语句前加上表名,这是不可取的。

另一方面,JOIN比EXISTS昂贵,而EXISTS更快,因为它不需要命令RDBMS获取任何数据,只需检查相关行是否存在。EXISTS用于返回布尔值,JOIN则返回整个其他表。

为了实现可扩展性,如果遵循分片架构,则必须删除JOIN。Pinterest在扩展期间实践过这种方法。 http://highscalability.com/blog/2013/4/15/scaling-pinterest-from-0-to-10s-of-billions-of-page-views-a.html


1

catsusers很可能都有一个名为id的列,这使得您建议的查询模糊不清。 Laravel的急切加载使用了额外的查询,但避免了这种潜在的错误。


9
在查询中使用别名(例如cat.id as cat__id)很容易处理,我希望ORM可以轻松地处理这个问题。 - IMSoP
1
@IMSoP,与某些ORM不同,Eloquent不知道表中有哪些列 - 大多数情况下它正在执行SELECT *。 它不会知道要使用哪些列别名。 - ceejayoz
3
请看我的评论,我提到了为每个连接返回一个额外的“分隔符”列(这是我在调试中经常使用的技巧)。构建一个ORM,在不提前知道非关键列的情况下使用连接是完全可能的。这要么是Eloquent在设计上做出的决定,要么只是他们不感兴趣修复的限制。 - IMSoP
@IMSoP 您可以在 GitHub 上向 Laravel 提交问题。我只是在解释当前的实现方式。 - ceejayoz
@ceejayoz,只是想指出它仍然只是一个“它不这样做”的问题,而不是“它不能这样做”的问题——实现可以完全对用户不可见,因此这是一个内部设计的问题。为什么它以这种方式工作的最合理解释似乎是简单性(实现本身)被视为比在幕后生成更复杂的SQL所能获得的效率更重要。 - IMSoP
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接