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"的页面 我想打印孩子们的总和(数量,大小?)只是为了那个电影院的电影,所以我这样写:

@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 什么都不说,每个都正常工作。

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

您可以使用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: truetickets.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 个查询