activerecord has_many:通过一个sql调用查找

bra*_*rad 4 activerecord ruby-on-rails

我有这3个型号:

class User < ActiveRecord::Base
  has_many :permissions, :dependent => :destroy
  has_many :roles, :through => :permissions
end

class Permission < ActiveRecord::Base
  belongs_to :role
  belongs_to :user
end
class Role < ActiveRecord::Base
  has_many :permissions, :dependent => :destroy
  has_many :users, :through => :permissions
end
Run Code Online (Sandbox Code Playgroud)

我想在一个sql语句中找到一个用户和它的角色,但我似乎无法实现这一点:

以下声明:

user = User.find_by_id(x, :include => :roles)
Run Code Online (Sandbox Code Playgroud)

给我以下问题:

  User Load (1.2ms)   SELECT * FROM `users` WHERE (`users`.`id` = 1) LIMIT 1
  Permission Load (0.8ms)   SELECT `permissions`.* FROM `permissions` WHERE (`permissions`.user_id = 1) 
  Role Load (0.8ms)   SELECT * FROM `roles` WHERE (`roles`.`id` IN (2,1)) 
Run Code Online (Sandbox Code Playgroud)

不完全理想.我如何做到这一点,以便它使用连接执行一个SQL查询并将用户的角色加载到内存中,这样说:

user.roles
Run Code Online (Sandbox Code Playgroud)

不会发出新的SQL查询

JRL*_*JRL 5

正如Damien指出的那样,如果你真的想要每次使用join时都需要一个查询.

但您可能不希望单个SQL调用.这就是为什么(从这里):

优化的预先加载


我们来看看这个:

Post.find(:all, :include => [:comments])
Run Code Online (Sandbox Code Playgroud)

在Rails 2.0之前,我们会在日志中看到类似下面的SQL查询:

SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `comments`.`id` AS t1_r0, `comments`.`body` AS t1_r1 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id 
Run Code Online (Sandbox Code Playgroud)

但是现在,在Rails 2.1中,相同的命令将提供不同的SQL查询.实际上至少2,而不是1."这怎么可能是一个改进?"让我们看一下生成的SQL查询:

SELECT `posts`.`id`, `posts`.`title`, `posts`.`body` FROM `posts` 

SELECT `comments`.`id`, `comments`.`body` FROM `comments` WHERE (`comments`.post_id IN (130049073,226779025,269986261,921194568,972244995))
Run Code Online (Sandbox Code Playgroud)

:include实现了Eager Loading 的关键字以解决可怕的1 + N问题.当您有关联时会发生此问题,然后您加载父对象并开始一次加载一个关联,从而导致1 + N问题.如果您的父对象有100个子对象,您将运行101个查询,这是不好的.尝试优化此方法的一种方法是使用OUTER JOINSQL中的子句连接所有内容,这样父内容对象和子对象在单个查询中一次加载.

看起来像一个好主意,实际上仍然是.但在某些情况下,怪物外连接比许多较小的查询慢.很多讨论一直在进行,你可以看看9640,9497,9560,L109门票的细节.

底线是:一般来说,将怪物连接分成较小的连接似乎更好,正如您在上面的例子中看到的那样.这避免了笛卡尔积的过载问题.对于没有经验的人,让我们运行查询的外连接版本:

mysql> SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `comments`.`id` AS t1_r0, `comments`.`body` AS t1_r1 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id ;

+-----------+-----------------+--------+-----------+---------+
| t0_r0     | t0_r1           | t0_r2  | t1_r0     | t1_r1   |
+-----------+-----------------+--------+-----------+---------+
| 130049073 | Hello RailsConf | MyText |      NULL | NULL    | 
| 226779025 | Hello Brazil    | MyText | 816076421 | MyText5 | 
| 269986261 | Hello World     | MyText |  61594165 | MyText3 | 
| 269986261 | Hello World     | MyText | 734198955 | MyText1 | 
| 269986261 | Hello World     | MyText | 765025994 | MyText4 | 
| 269986261 | Hello World     | MyText | 777406191 | MyText2 | 
| 921194568 | Rails 2.1       | NULL   |      NULL | NULL    | 
| 972244995 | AkitaOnRails    | NULL   |      NULL | NULL    | 
+-----------+-----------------+--------+-----------+---------+
8 rows in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

注意这一点:你在前3列(t0_r0到t0_r2)中看到了很多重复吗?这些是Post模型列,剩下的是每个帖子的评论列.请注意,"Hello World"帖子重复了4次.这就是连接的作用:为每个子节点重复父行.那个帖子有4个评论,所以重复了4次.

问题是,这很难打到Rails,因为它必须处理几个小而短暂的对象.在Rails方面感受到了痛苦,而在MySQL方面却没有那么多.现在,将其与较小的查询进行比较:

mysql> SELECT `posts`.`id`, `posts`.`title`, `posts`.`body` FROM `posts` ;
+-----------+-----------------+--------+
| id        | title           | body   |
+-----------+-----------------+--------+
| 130049073 | Hello RailsConf | MyText | 
| 226779025 | Hello Brazil    | MyText | 
| 269986261 | Hello World     | MyText | 
| 921194568 | Rails 2.1       | NULL   | 
| 972244995 | AkitaOnRails    | NULL   | 
+-----------+-----------------+--------+
5 rows in set (0.00 sec)

mysql> SELECT `comments`.`id`, `comments`.`body` FROM `comments` WHERE (`comments`.post_id IN (130049073,226779025,269986261,921194568,972244995));
+-----------+---------+
| id        | body    |
+-----------+---------+
|  61594165 | MyText3 | 
| 734198955 | MyText1 | 
| 765025994 | MyText4 | 
| 777406191 | MyText2 | 
| 816076421 | MyText5 | 
+-----------+---------+
5 rows in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

实际上我有点作弊,我手动删除了上述所有查询中的created_at和updated_at字段,以便您更清楚地理解它.所以,你有它:帖子结果集,分隔和不重复,评论结果集与以前相同的大小.结果集越长越复杂,这就越重要,因为Rails需要处理的对象越多.分配和解除分配数百或数千个小型重复对象绝非易事.

但这个新功能很聪明.假设你想要这样的东西:

>> Post.find(:all, :include => [:comments], :conditions => ["comments.created_at > ?", 1.week.ago.to_s(:db)])
Run Code Online (Sandbox Code Playgroud)

在Rails 2.1中,它会理解'comments'表有一个过滤条件,因此它不会将其分解为小查询,而是生成旧的外连接版本,如下所示:

SELECT `posts`.`id` AS t0_r0, `posts`.`title` AS t0_r1, `posts`.`body` AS t0_r2, `posts`.`created_at` AS t0_r3, `posts`.`updated_at` AS t0_r4, `comments`.`id` AS t1_r0, `comments`.`post_id` AS t1_r1, `comments`.`body` AS t1_r2, `comments`.`created_at` AS t1_r3, `comments`.`updated_at` AS t1_r4 FROM `posts` LEFT OUTER JOIN `comments` ON comments.post_id = posts.id WHERE (comments.created_at > '2008-05-18 18:06:34') 
Run Code Online (Sandbox Code Playgroud)

因此,连接表上的嵌套连接,条件等应该仍然可以正常工作.总的来说,它应该加快你的查询.一些人报告说,由于更多的个人查询,MySQL似乎在CPU方面受到更强烈的打击.你在家工作,并进行压力测试和基准测试,看看会发生什么.


Luk*_*ncl 5

在单独的SQL查询中加载角色实际上是一种称为"优化的预先加载"的优化.

Role Load (0.8ms)   SELECT * FROM `roles` WHERE (`roles`.`id` IN (2,1))
Run Code Online (Sandbox Code Playgroud)

(这样做而不是单独加载每个角色,N + 1问题.)

Rails团队发现使用IN查询通常更快,之前查找的关联代替进行大连接.

只有在其他表之一上添加条件时,才会在此查询中进行连接.Rails会检测到这一点并进行连接.

例如:

User.all(:include => :roles, :conditions => "roles.name = 'Admin'")
Run Code Online (Sandbox Code Playgroud)

查看原始票证,此前Stack Overflow问题以及Fabio Akita关于Optimized Eager Loading的博文.