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
<% @childrens.each do |children| %><%= children.movies.size %><% end %>
但显然我有子弹 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: true
,tickets.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.movies 为例,您需要先将 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”查询?的主要内容,如果未能解决你的问题,请参考以下文章