如何让Rails获得急切的负载计数?

时间:2011-02-05 18:51:30

标签: ruby-on-rails activerecord eager-loading active-relation

这与问题a year and change ago有关。

我提供了一个开箱即用的问题示例,只要您有sqlite3可用:https://github.com/cairo140/rails-eager-loading-counts-demo

安装说明(主分支)

git clone git://github.com/cairo140/rails-eager-loading-counts-demo.git
cd rails-eager-loading-counts-demo
rails s

我在存储库中有更全面的文章,但我的一般问题是这个。

如何以最小化数据库查询的方式使Rails急切加载计数?

尽管在ActiveRelation中通过n+1包含了该关联,但只要您在关联上使用#count,就会出现#includes(:associated)问题。一种解决方法是使用#length,但只有当它被调用的对象已被加载时才能正常工作,更不用说我怀疑它复制了Rails内部已经完成的东西。此外,使用#length的一个问题是,当关联未加载时,会导致不幸的过载,并且只需要计数。

自述文件:

  

我们可以通过在已经加载的posts数组(参见附录)上运行#length来避免这个问题,但是也可以随时获得数量。它不仅更加一致;它提供了一个访问路径,不一定需要加载帖子。例如,如果你有一个部分显示计数,无论什么,但有一半的时间,部分是在加载的帖子和一半的时间没有调用,你面临以下情况:

     
      
  • 使用#count      
        邮件已加载时
    • n COUNT样式查询
    •   
    • n COUNT样式查询尚未加载帖子时
    •   
  •   
  • 使用#length      
        
    • 在已加载帖子时将其他查询归零
    •   
    • n *样式查询尚未加载帖子时
    •   
  •   
     

在这两种选择之间,没有显性选择。但是修改#count以推迟到#length或访问存储在幕后的其他方式的长度会很好,这样我们就可以有以下场景:

     
      
  • 使用修订后的#count      
        
    • 在已加载帖子时将其他查询归零
    •   
    • n COUNT样式查询尚未加载帖子时
    •   
  •   

那么这里的正确方法是什么?有没有我忽略的东西(非常非常可能)?

4 个答案:

答案 0 :(得分:7)

正如@apneadiving建议的那样,counter_cache运行良好,因为在添加或删除记录时,计数器列会自动更新。因此,当您加载父对象时,计数将包含在对象中,而无需访问其他表。

但是,如果由于某种原因你不喜欢这种方法,你可以这样做:

Post.find(:all,
          :select => "posts.*, count(comments.id) `comments_count`",
          :joins  => "left join comments on comments.post_id = posts.id")

答案 1 :(得分:2)

看来,实现此类工具的最佳方法可能是为您想要的单独的模型和子计数对象创建SQL视图(参考:herehere) ;及其相关的ActiveRecord模型。

您可能能够非常聪明并在原始模型上使用子类化并结合set_table_name :sql_view_name来保留对象上的所有原始方法,甚至可能保留一些关联。

例如,假设我们要在您的示例中添加“Post.has_many:comments”,就像上面@ Zubin的回答一样;一个人可能会这样做:

   class CreatePostsWithCommentsCountsView < ActiveRecord::Migration
      def self.up
        #Create SQL View called posts_with_comments_counts which maps over 
        # select posts.*, count(comments.id) as comments_count from posts 
        #   left outer join comments on comments.post_id = posts.id 
        #   group by posts.id
        # (As zubin pointed out above.) 
        #*Except* this is in SQL so perhaps we'll be able to do further 
        # reducing queries against it *as though it were any other table.*
      end    
   end

   class PostWithCommentsCount < Post         #Here there be cleverness.
                                              #The class definition sets up PWCC 
                                              # with all the regular methods of 
                                              # Post (pointing to the posts table
                                              # due to Rails' STI facility.)

    set_table_name :posts_with_comment_counts #But then we point it to the 
                                              # SQL view instead.
                                              #If you don't really care about
                                              # the methods of Post being in PWCC
                                              # then you could just make it a 
                                              # normal subclass of AR::Base.
   end

   PostWithCommentsCount.all(:include => :user)  #Obviously, this sort of "upward
     # looking" include is best used in big lists like "latest posts" rather than
     # "These posts for this user." But hopefully it illustrates the improved 
     # activerecordiness of this style of solution.
   PostWithCommentsCount.all(:include => :comments) #And I'm pretty sure you 
     # should be able to do this without issue as well. And it _should_ only be 
     # the two queries.

答案 2 :(得分:2)

我已经设置了一个小型gem,它向ActiveRecord添加了一个includes_count方法,该方法使用SELECT COUNT来获取关联中的记录数,而不需要使用可能很昂贵的JOIN(取决于情况)。

请参阅https://github.com/manastech/includes-count

希望它有所帮助!

答案 3 :(得分:2)

Zubin的另一种方法:

Post.select('posts.*, count(comments.id) `comments_count`').joins(:comments).group('posts.id')