Rails,如何避免对关联中的总数(计数、大小、counter_cache)进行“N + 1”查询?

Posted

技术标签:

【中文标题】Rails,如何避免对关联中的总数(计数、大小、counter_cache)进行“N + 1”查询?【英文标题】:Rails, how to avoid the "N + 1" queries for the totals (count, size, counter_cache) in associations? 【发布时间】: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 什么都没说,一切正常。

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

【问题讨论】:

【参考方案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,因为它运行一个单独的查询并且不会修改我的原始查询,如果查询本身是已经相当复杂了。

【讨论】:

您的回答似乎很有趣!您如何看待这个答案(***.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 请求

【讨论】:

以上是关于Rails,如何避免对关联中的总数(计数、大小、counter_cache)进行“N + 1”查询?的主要内容,如果未能解决你的问题,请参考以下文章

由关联计数组成的 Rails 查询,最好使用范围

如何通过rails中的关联定义has_many

Rails has_many 关联计数子行

Backbone 和 Rails 关联:避免 JSON HashWithIndifferentAccess 错误

在Rails 5.1模板中显示pg_search的循环计数

Rails - 仅根据具体情况引入一些 HABTM 关联,​​以避免不必要的连接