Как получить записи с точным has_many через количество записей на рельсах

У меня есть отношение многие ко многим через has_many через

class Person < ActiveRecord::Base
  has_many :rentals
  has_many :books, through rentals
end

class Rentals < ActiveRecord::Base
  belongs_to :book
  belongs_to :person
end

class Book < ActiveRecord::Base
  has_many :rentals
  has_many :persons, through rentals
end

Как я могу получить людей, у которых есть только одна книга?


person Petran    schedule 19.12.2018    source источник


Ответы (2)


Если таблица для Person называется persons, вы можете построить соответствующий SQL-запрос, используя DSL запросов ActiveRecord:

people_with_book_ids = Person.joins(:books)
                             .select('persons.id')
                             .group('persons.id')
                             .having('COUNT(books.id) = 1')
Person.where(id: people_with_book_ids)

Хотя это две строки кода Rails, ActiveRecord объединит их в один вызов базы данных. Если вы запустите его в консоли Rails, вы можете увидеть оператор SQL, который выглядит примерно так:

SELECT "persons".* FROM "persons" WHERE "deals"."id" IN 
(SELECT persons.id FROM "persons" INNER JOIN "rentals" 
ON "rentals"."person_id" = "persons"."id"
INNER JOIN "books" ON "rentals"."book_id" = "books"."id" 
GROUP BY persons.id HAVING count(books.id) > 1)
person Scott Matthewman    schedule 19.12.2018
comment
Я полагаю, что Rails фактически сгруппирует эти два запроса в один SQL-запрос, используя подзапрос. Если это область кодовой базы с высокой посещаемостью, было бы целесообразно заменить строки более гибкими областями (потенциально используя Arel для подсчетов и имен столбцов). В конечном итоге это позволяет использовать что-то вроде books_that_are_the_only_rental = Book.where(persons: Person.with_one_book). - person coreyward; 19.12.2018
comment
Вы вполне можете быть правы! На самом деле я хотел добавить .map(&:id) в конец people_with_book_ids, что заставило бы операцию выполняться в два запроса (с риском возникновения проблем с масштабированием, если у вас много людей в БД). Пропустить это может быть счастливой случайностью :) - person Scott Matthewman; 19.12.2018
comment
Да, подзапрос работает только в том случае, если подзапрос возвращает одно поле: исправлен мой код и объяснение. Спасибо, @coreyward! - person Scott Matthewman; 19.12.2018
comment
Примечания: множественное число от person будет people (активная_поддержка справится с этим), у вас есть сделки в запросе, не знаю почему, и условие наличия в вашей первой части не соответствует второму. - person engineersmnky; 19.12.2018
comment
Почему бы не напрямую .select('persons.*'), чтобы вам не нужен второй оператор для Person.where(id: people_with_book_ids)? - person MrYoshiji; 19.12.2018

Если вы хотите делать это часто, Rails предлагает то, что называется кэш счетчика:

Опцию :counter_cache можно использовать для более эффективного определения количества принадлежащих объектов.

С этим объявлением Rails будет обновлять значение кэша, а затем возвращать это значение в ответ на метод size.

По сути, это помещает в ваш Person новый атрибут с именем books_count, который позволит вам довольно просто фильтровать по количеству связанных книг:

Person.where(books_count: 1)
person coreyward    schedule 19.12.2018
comment
Да, если это тип запроса, который будет часто использоваться (и с другим books_count), кэш-счетчик может быть полезен! Однако есть несколько ошибок при использовании counter_cache с has_many :throughэтот ответ SO содержит несколько полезных указателей. - person Scott Matthewman; 19.12.2018