【问题标题】:Rails, how to avoid the "N + 1" queries for the totals (count, size, counter_cache) in associations?Rails,如何避免对关联中的总数(计数、大小、counter_cache)进行“N + 1”查询?
【发布时间】:2015-09-21 13:07:09
【问题描述】:

我有这些型号:

class Children < ActiveRecord::Base
    has_many :tickets
    has_many :movies, through: :tickets
end


class Movie < ActiveRecord::Base
    has_many :tickets
    has_many :childrens, through: :tickets
    belongs_to :cinema
end


class Ticket < ActiveRecord::Base
    belongs_to :movie, counter_cache: true
    belongs_to :children
end


class Cinema < ActiveRecord::Base
    has_many :movies, dependent: :destroy
    has_many :childrens, through: :movies
end

我现在需要的是在“电影院”的页面中,我想为电影院的电影打印孩子们的总和(计数,大小?),所以我写了这个:

  • 在cinemas_controller.rb中

@childrens = @cinema.childrens.uniq

  • 在电影院/show.html.erb

&lt;% @childrens.each do |children| %&gt;&lt;%= children.movies.size %&gt;&lt;% end %&gt;

但显然我有子弹 gem 提醒我 Counter_cache 并且我不知道将这个 counter_cache 放在哪里,因为电影的 id 不同。

而且,如果没有 counter_cache,我所拥有的也不是我想要的,因为我想计算那家电影院有多少孩子从那家电影院的票中拿走他们。

怎么做?

更新

如果在我看来我使用此代码:

<% @childrens.each do |children| %>
  <%= children.movies.where(cinema_id: @cinema.id).size %>
<% end %>

gem bullet 什么都没说,一切正常。

但是我有一个问题:这种查询数据库的方式因为视图中的代码比较重?

【问题讨论】:

    标签: ruby-on-rails ruby ruby-on-rails-4 activerecord relationship


    【解决方案1】:

    这可能会对你有所帮助。

    @childrens_count = @cinema.childrens.joins(:movies).group("movies.children_id").count.to_a
    

    【讨论】:

    • 你能解释清楚吗?
    • @JohnSam。哦,对不起,分组有问题。请将movies.children_id 更改为ticket.children_id。输出将包含每个孩子的电影总和。在视图部分,您可以使用此输出数组来显示数据。
    • 是的,我更正了。有趣的是如何在我的网格中显示结果(列表,index.html.erb)。
    • 你可以类似这样使用,
    • 也许以.count.to_h 结尾会更好(to_h)
    【解决方案2】:

    您可以使用includes 提前加载所有关联。例如:

    @childrens = @cinema.childrens.includes(:movies).uniq
    

    这将在控制器中加载所有孩子的电影,防止视图需要访问循环中的数据库。

    【讨论】:

    • 是的,但我需要总数。还包括我有 counter_cache 的子弹 gem 错误和很多查询。
    • 啊,我明白了。也许this answer 会给你你所需要的。
    • 我不这么认为。因为不是我需要的。我已经有了 counter_cache。
    【解决方案3】:

    您可能同意,属于孩子的电影数量等于他们购买的门票数量。 这就是为什么您可以只缓存门票数量并将其显示在电影院#show 上。 你甚至可以创建一个方法让它更清晰。

    class Children < ActiveRecord::Base
      has_many :tickets
      has_many :movies, through: :tickets
    
      def movies_count
        tickets.size
      end
    end
    
    class Ticket < ActiveRecord::Base
      belongs_to :movie, counter_cache: true
      belongs_to :children, counter_cache: true
    end
    
    class Movie < ActiveRecord::Base
      belongs_to :cinema
      has_many :tickets
      has_many :childrens, through: :tickets
    end
    
    class Cinema < ActiveRecord::Base
      has_many :movies, dependent: :destroy
      has_many :childrens, through: :movies
    end
    

    然后:

    <% @childrens.each do |children| %><%= children.tickets.size %><% end %>
    

    或者

    <% @childrens.each do |children| %><%= children.movies_count %><% end %>
    

    但如果你想显示每部电影的票数,你肯定需要考虑以下几点:

    @movies = @cinema.movies
    

    然后: <% @movies.each do |movie| %><%= movie.tickets.size %><% end %> 由于您有belongs_to :movie, counter_cache: truetickets.size 不会进行计数查询。 并且不要忘记添加tickets_count 列。 More about counter_cache...

    附:请注意,根据惯例,我们将模型命名为 Child,将关联命名为 Children。

    【讨论】:

    • 亲爱的@Ihar Drozdov,我不能同意你的看法,因为我需要计算每部电影的票数,而不是所有电影的总数!
    • 不。你还是不明白我的问题。请参阅我的问题的编辑部分。我需要在电影院的放映页面中计算该电影的每个孩子的总票数。没有每个孩子的每个查询。现在问题更清楚了吗?
    【解决方案4】:

    我前段时间写了一个小 ActiveRecord 插件,但还没有机会发布 gem,所以我只是创建了一个 gist:

    https://gist.github.com/apauly/38f3e88d8f35b6bcf323

    例子:

    # The following code will run only two queries - no matter how many childrens there are:
    #   1. Fetch the childrens
    #   2. Single query to fetch all movie counts
    @cinema.childrens.preload_counts(:movies).each do |cinema|
      puts cinema.movies.count
    end
    

    再解释一下:

    已经有类似的解决方案(例如https://github.com/smathieu/preload_counts),但我不喜欢他们的接口/DSL。我一直在寻找类似于活动记录 preload (http://apidock.com/rails/ActiveRecord/QueryMethods/preload) 方法的东西(在语法上),这就是我创建自己的解决方案的原因。

    为了避免“正常”的 N+1 查询问题,我总是使用 preload 而不是 joins,因为它运行一个单独的查询并且不会修改我的原始查询,如果查询本身是已经相当复杂了。

    【讨论】:

    • 您的回答似乎很有趣!您如何看待这个答案(stackoverflow.com/a/31313129/4412054),我修改了一点(@childrens_count = @cinema.childrens.group("tickets.children_id").size)以便在我的情况下使用它。它只是工作!您的解决方案有何不同?
    • 糟糕,我似乎只是发布了另一个答案而不是编辑原始答案,所以我只是删除了旧版本。
    • 你能回答我的问题吗?
    • 做法基本一样
    • 诀窍是在使用延迟加载时使用.size,就像在我的回答中一样
    【解决方案5】:

    其实比剩下的解决方案简单很多

    你可以使用lazy loading:

    在您的控制器中:

    def index
      # or you just add your where conditions here
      @childrens = Children.includes(:movies).all 
     end
    

    在你看来index.hml.erb

    <% @childrens.each do |children| %>
      <%= children.movies.size %>
    <% end %>
    

    如果您使用size,上面的代码不会进行任何额外查询,但如果您使用count,您将面临select count(*) n + 1 个查询

    【讨论】:

      【解决方案6】:

      在你的情况下,你可以使用这样的东西:

      class Ticket < ActiveRecord::Base
          belongs_to :movie, counter_cache: true
          belongs_to :children
      end
      class Movie < ActiveRecord::Base
          has_many :tickets
          has_many :childrens, through: :tickets
          belongs_to :cinema
      end
      class Children < ActiveRecord::Base
          has_many :tickets
          has_many :movies, through: :tickets
      end
      class Cinema < ActiveRecord::Base
          has_many :movies, dependent: :destroy
          has_many :childrens, through: :movies
      end
      
      
      @cinema = Cinema.find(params[:id])
      @childrens = Children.eager_load(:tickets, :movies).where(movies: {cinema_id: @cinema.id}, tickets: {cinema_id: @cinema.id})
      
      
      <% @childrens.each do |children| %>
        <%= children.movies.count %>
      <% end %>
      

      【讨论】:

      • 票属于电影,而不是电影院。
      • 而且,总体而言,您的代码不起作用。我还有很多疑问,每个孩子一个。
      • 我写了You could use something like this。我不知道你是如何通过迁移创建表的......在我的项目中,好的是“eager_load”,它可以正常工作。但是在您的项目中,它可能是“连接”或“包含”——与您的数据库模式相关...主要思想——您使用连接“大表”来过滤某些电影院拥有的电影和门票。接下来你计算有多少电影有特定的孩子.. 有什么问题?
      【解决方案7】:

      您使用counter_cache 的方法是正确的。

      但要充分利用它,我们以 children.movi​​es 为例,您需要先将 tickets_count 列添加到 children 表中。

      执行rails g migration addTicketsCountToChildren tickets_count:integer

      然后rake db:migrate

      现在每张票创建都会自动将其所有者(孩子)中的票数增加 1。

      那么你可以使用

      <% @childrens.each do |children| %>
        <%= children.movies.size %>
      <% end %>
      

      没有收到任何警告。

      如果你想按电影统计孩子,你需要将childrens_count添加到movie表中:

      rails g migration addChildrensCountToMovies childrens_count:integer
      

      然后rake db:migrate

      参考:

      http://yerb.net/blog/2014/03/13/three-easy-steps-to-using-counter-caches-in-rails/

      如果有任何问题,请随时询问。

      【讨论】:

      • 亲爱的@传品,我需要的不是你写的。我需要找到一种在我的cinema/show.html.erb 中使用此代码不进行N+1 查询的方法。因为我想显示一张儿童票的名单(在很多天),并且对于该名单中的每个孩子,我想计算有多少票。请阅读我的问题的编辑部分。
      【解决方案8】:

      基于sarav answer,如果您有很多事情(请求)要计算,您可以做:

      在控制器中:

      @childrens_count = @cinema.childrens.joins(:movies).group("childrens.id").count.to_h

      在视图中:

      <% @childrens.each do |children| %>
         <%= @childrens_count[children.id] %>
      <% end %>
      

      如果你训练计算关联记录,这将阻止大量 sql 请求

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-10-24
        • 2015-03-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多