Ruby on rails activerecord有很多:通过一个sql调用进行查找

Ruby on rails activerecord有很多:通过一个sql调用进行查找,ruby-on-rails,activerecord,Ruby On Rails,Activerecord,我对这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 &

我对这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
我有以下疑问:

  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)) 
不太理想。我如何做到这一点,以便它使用联接执行一个sql查询,并将用户的角色加载到内存中,例如:

user.roles

不发出新的sql查询,包括加载数据的模型。但进行第二次查询。
对于要执行的操作,应使用
:joins
参数

user = User.find_by_id(x, :joins => :roles)

正如Damien所指出的,如果每次都需要一个查询,那么应该使用join

但您可能不需要一个SQL调用。以下是原因:

优化的即时加载

让我们来看看这个:

Post.find(:all, :include => [:comments])
在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 
但是现在,在Rails2.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))
为了解决可怕的1+N问题,实现了用于急切加载的
:include
关键字。当您有关联,然后加载父对象并开始一次加载一个关联时,就会出现此问题,因此出现1+N问题。如果父对象有100个子对象,那么将运行101个查询,这是不好的。尝试对此进行优化的一种方法是使用SQL中的
outerjoin
子句连接所有对象,这样在单个查询中同时加载父对象和子对象

这似乎是个好主意,实际上仍然是。但在某些情况下,monster外部连接比许多较小的查询速度慢。很多讨论都在进行中,您可以在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)
请注意:您是否在前3列(t0_r0到t0_r2)中看到大量重复?这些是Post模型列,其余的是每个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)
实际上我有点作弊,我手动从所有上述查询中删除了created_at和updated_at字段,以便您更清楚地理解它。这样,您就有了:posts结果集,分离且不重复,comments结果集的大小与以前相同。结果集越长越复杂,这就越重要,因为Rails需要处理的对象越多。分配和取消分配数百或数千个小的重复对象从来都不是一件好事

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

>> Post.find(:all, :include => [:comments], :conditions => ["comments.created_at > ?", 1.week.ago.to_s(:db)])
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') 
在Rails 2.1中,它会理解“comments”表有一个过滤条件,因此它不会将其分解为小查询,而是生成旧的外部联接版本,如下所示:

>> Post.find(:all, :include => [:comments], :conditions => ["comments.created_at > ?", 1.week.ago.to_s(:db)])
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') 

因此,联接表上的嵌套联接、条件等仍然可以正常工作。总的来说,它会加快你的查询速度。一些人报告说,由于更多的单个查询,MySQL似乎在CPU方面受到了更大的冲击。您是否在家工作,并进行压力测试和基准测试,以了解发生了什么。

在单独的SQL查询中加载角色实际上是一种称为“优化急切加载”的优化

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

Rails团队发现,使用带有先前查找的关联的IN查询通常比使用大型联接更快

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

例如:

User.all(:include => :roles, :conditions => "roles.name = 'Admin'")

请参阅、this和Fabio Akita关于的博客文章。

但是user.roles还是会进行第二次查询,所以这根本没有帮助。感谢您的深入解释。我知道大的连接是昂贵的,但是,我的代码发现这个用户是在一个before过滤器中从会话中获取当前用户,所以我只选择一个用户,所以没有大的连接。我会认为,对于查找具有多个角色的单个用户而言,通过includes进行连接会更快。有什么建议吗?你总是可以用sql查找你想要的内容。