Rails, как избежать запросов "N + 1" для итогов (count, size, counter_cache) в ассоциациях?

У меня есть эти модели:

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:

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

но, очевидно, у меня есть маркер bullet, который предупреждает меня о Counter_cache, и я не знаю, куда поместить этот counter_cache из-за другого идентификатора для фильма.

А также без counter_cache то, что у меня есть, - это не то, что я хочу, потому что я хочу подсчитать, сколько детей в этом кино берут их из билетов из многих дней в этом кинотеатре.

Как?

UPDATE

Если, на мой взгляд, я использую этот код:

<% @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

Ответ 2

Вы можете использовать includes для загрузки всех ассоциаций заблаговременно. Например:

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

Это загрузит все детские фильмы в контроллере, не позволяя виду получать доступ к базе данных в вашем цикле.

Ответ 3

Вы можете согласиться с тем, что количество фильмов принадлежит ребенку, равное количеству купленных им билетов. Вот почему вы можете просто кэшировать количество билетов и показывать их на шоу # кинотеатров. Вы даже можете создать метод, чтобы сделать его более понятным.

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. Подробнее о counter_cache...

P.S. Просто примечание, в соответствии с условными обозначениями, мы называем модель Child и ассоциацией в качестве детей.

Ответ 4

Я написал небольшой плагин ActiveRecord некоторое время назад, но у меня не было возможности опубликовать драгоценный камень, поэтому я просто создал суть:

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, потому что он запускает один отдельный запрос и не изменяет мой первоначальный запрос, который может сломаться, если сам запрос уже довольно сложный.

Ответ 5

В вашем случае вы можете использовать что-то вроде этого:

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 %>

Ответ 6

Ваш подход с использованием 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/

пожалуйста, не стесняйтесь спрашивать, есть ли какие-либо проблемы.

Ответ 7

На основе sarav answer, если у вас есть много вещей (запросов), которые вы можете подсчитать:

в контроллере:

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

в поле зрения:

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

Это предотвратит много запросов sql, если вы тренируете подсчет связанных записей

Ответ 8

На самом деле гораздо проще, чем остальные решения

Вы можете использовать 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 запросами