Ответ 1
Это может вам помочь.
@childrens_count = @cinema.childrens.joins(:movies).group("movies.children_id").count.to_a
У меня есть эти модели:
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
Теперь мне нужно перейти на страницу "Кинотеатры". Я хочу напечатать сумму (количество, размер?) детей только для фильмов в этих кинотеатрах, поэтому я написал следующее:
@childrens = @cinema.childrens.uniq
<% @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 ничего мне не говорит, и все работает правильно.
Но у меня есть вопрос: этот способ запроса к базе данных более тяжелый из-за кода в представлениях?
Это может вам помочь.
@childrens_count = @cinema.childrens.joins(:movies).group("movies.children_id").count.to_a
Вы можете использовать includes для загрузки всех ассоциаций заблаговременно. Например:
@childrens = @cinema.childrens.includes(:movies).uniq
Это загрузит все детские фильмы в контроллере, не позволяя виду получать доступ к базе данных в вашем цикле.
Вы можете согласиться с тем, что количество фильмов принадлежит ребенку, равное количеству купленных им билетов. Вот почему вы можете просто кэшировать количество билетов и показывать их на шоу # кинотеатров. Вы даже можете создать метод, чтобы сделать его более понятным.
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 и ассоциацией в качестве детей.
Я написал небольшой плагин 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
, потому что он запускает один отдельный запрос и не изменяет мой первоначальный запрос, который может сломаться, если сам запрос уже довольно сложный.
В вашем случае вы можете использовать что-то вроде этого:
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
.
выполнить 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/
пожалуйста, не стесняйтесь спрашивать, есть ли какие-либо проблемы.
На основе sarav answer, если у вас есть много вещей (запросов), которые вы можете подсчитать:
в контроллере:
@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 запросами