【问题标题】:activerecord has_many :through find with one sql callactiverecord has_many :通过一次 sql 调用查找
【发布时间】:2010-12-14 16:33:29
【问题描述】:

我有这 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

我想在一个 sql 语句中找到一个用户及其角色,但我似乎无法做到这一点:

以下声明:

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

给我以下查询:

  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 查询

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

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

    Role Load (0.8ms)   SELECT * FROM `roles` WHERE (`roles`.`id` IN (2,1))
    

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

    Rails 团队发现,使用 IN 查询和之前查找的关联通常更快,而不是进行大连接。

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

    例如:

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

    请参阅original ticket、previous Stack Overflow question 和 Fabio Akita 关于Optimized Eager Loading 的博文。

    【讨论】:

      【解决方案2】:

      正如 Damien 指出的那样,如果您真的想要每次都使用一个查询,则应该使用 join。

      但您可能不需要单个 SQL 调用。原因如下(来自here):

      优化预加载


      我们来看看这个:

      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 
      

      但是现在,在 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))
      

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

      看起来是个好主意,实际上仍然是。但在某些情况下,怪物外连接变得比许多较小的查询慢。很多讨论正在进行中,您可以在门票 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)中有很多重复?这些是帖子模型列,其余的是每个帖子的评论列。请注意,“Hello World”帖子重复了 4 次。这就是连接的作用:为每个孩子重复父行。该特定帖子有 4 个 cmets,因此重复了 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 结果集,分离且不重复,cmets 结果集与以前的大小相同。结果集越长越复杂,这一点就越重要,因为 Rails 必须处理的对象越多。分配和释放成百上千个小的重复对象从来都不是什么好事。

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

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

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

      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 方面获得了更强的冲击力。您是否在家工作并进行压力测试和基准测试,看看会发生什么。

      【讨论】:

      • 感谢您的深入解释。我知道大型联接很昂贵,但是,我发现此用户的代码位于从会话中获取 current_user 的前置过滤器中,因此我只选择一个用户,因此没有大型联接。我会认为,通过包含加入会更快地找到具有多个角色的单个用户。关于如何做到这一点的任何建议?
      • 您可以随时执行 find_by_sql 并输入您想要的任何 SQL。
      【解决方案3】:

      包含模型会加载数据。但进行第二次查询。
      对于你想做的事情,你应该使用:joins参数。

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

      【讨论】:

      • 但是 user.roles 还是会进行第二次查询,所以这根本没有帮助
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-02
      • 1970-01-01
      • 1970-01-01
      • 2018-05-07
      • 2013-08-16
      相关资源
      最近更新 更多