Rails,如何避免"N + 1"查询关联中的总计(计数,大小,counter_cache)?
Rails, how to avoid the "N + 1" queries for the totals (count, size, counter_cache) in associations?
我有这些型号:
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"的页面 我想打印孩子们的总和(数量,大小?)只是为了那个电影院的电影,所以我这样写:
- 中cinemas_controller.rb:
@childrens = @cinema.childrens.uniq
- 中cinemas/show.html.erb:
<% @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 什么都不说,每个都正常工作。
但是我有一个疑问:这种查询数据库的方式是因为视图中的代码比较重?
您可以使用includes提前加载所有关联。例如:
@childrens = @cinema.childrens.includes(:movies).uniq
这将在控制器中加载所有儿童电影,防止视图需要访问循环中的数据库。
您可能会同意,child 电影的数量等于他们购买的门票数量。
这就是为什么你可以只缓存票数并在电影院#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...
P.S。请注意,根据惯例,我们将模型命名为 Child,将关联命名为 Children.
在你的情况下你可以使用这样的东西:
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 %>
您使用 counter_cache
的方法是正确的。
但要充分利用它,让我们以 children.movies 为例,您需要先将 tickets_count
列添加到 children
table。
执行rails g migration addTicketsCountToChildren tickets_count:integer
,
然后 rake db:migrate
现在每张创建的工单都会自动将其所有者(children)中的tickets_count增加1。
然后你可以使用
<% @childrens.each do |children| %>
<%= children.movies.size %>
<% end %>
没有得到任何警告。
如果你想得到 children 按电影计数,你需要将 childrens_count
添加到 movie
table:
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/
如有任何疑虑,请随时询问。
这可能对您有所帮助。
@childrens_count = @cinema.childrens.joins(:movies).group("movies.children_id").count.to_a
前段时间写了一个小的ActiveRecord插件,但是一直没机会发布gem,所以就写了一个要点:
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) but I didn't like their interface/DSL. I was looking for something (syntactically) similar to active records preload
(http://apidock.com/rails/ActiveRecord/QueryMethods/preload)方法,这就是我创建自己的解决方案的原因。
为了避免 'normal' N+1 查询问题,我总是使用 preload
而不是 joins
因为它运行一个单独的查询并且不会修改我的原始查询如果查询本身已经相当复杂,可能会中断。
基于 如果你有很多事情(请求)要统计你可以这样做:
在控制器中:
@childrens_count = @cinema.childrens.joins(:movies).group("childrens.id").count.to_h
可见:
<% @childrens.each do |children| %>
<%= @childrens_count[children.id] %>
<% end %>
如果您训练计算相关记录
,这将防止很多 sql 请求
实际上比其余的解决方案要简单得多
您可以使用 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 个查询
我有这些型号:
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"的页面 我想打印孩子们的总和(数量,大小?)只是为了那个电影院的电影,所以我这样写:
- 中cinemas_controller.rb:
@childrens = @cinema.childrens.uniq
- 中cinemas/show.html.erb:
<% @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 什么都不说,每个都正常工作。
但是我有一个疑问:这种查询数据库的方式是因为视图中的代码比较重?
您可以使用includes提前加载所有关联。例如:
@childrens = @cinema.childrens.includes(:movies).uniq
这将在控制器中加载所有儿童电影,防止视图需要访问循环中的数据库。
您可能会同意,child 电影的数量等于他们购买的门票数量。 这就是为什么你可以只缓存票数并在电影院#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...
P.S。请注意,根据惯例,我们将模型命名为 Child,将关联命名为 Children.
在你的情况下你可以使用这样的东西:
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 %>
您使用 counter_cache
的方法是正确的。
但要充分利用它,让我们以 children.movies 为例,您需要先将 tickets_count
列添加到 children
table。
执行rails g migration addTicketsCountToChildren tickets_count:integer
,
然后 rake db:migrate
现在每张创建的工单都会自动将其所有者(children)中的tickets_count增加1。
然后你可以使用
<% @childrens.each do |children| %>
<%= children.movies.size %>
<% end %>
没有得到任何警告。
如果你想得到 children 按电影计数,你需要将 childrens_count
添加到 movie
table:
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/
如有任何疑虑,请随时询问。
这可能对您有所帮助。
@childrens_count = @cinema.childrens.joins(:movies).group("movies.children_id").count.to_a
前段时间写了一个小的ActiveRecord插件,但是一直没机会发布gem,所以就写了一个要点:
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) but I didn't like their interface/DSL. I was looking for something (syntactically) similar to active records preload
(http://apidock.com/rails/ActiveRecord/QueryMethods/preload)方法,这就是我创建自己的解决方案的原因。
为了避免 'normal' N+1 查询问题,我总是使用 preload
而不是 joins
因为它运行一个单独的查询并且不会修改我的原始查询如果查询本身已经相当复杂,可能会中断。
基于
在控制器中:
@childrens_count = @cinema.childrens.joins(:movies).group("childrens.id").count.to_h
可见:
<% @childrens.each do |children| %>
<%= @childrens_count[children.id] %>
<% end %>
如果您训练计算相关记录
,这将防止很多 sql 请求实际上比其余的解决方案要简单得多
您可以使用 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 个查询