RubyOnRails состав нескольких областей
Я получил модель продукта с таблицей типов has_many и несколькими областями действия:
class Product < ActiveRecord::Base
has_many :product_types
has_many :types, through: :product_types
scope :type1, -> { joins(:types).where(types: { name: "Type1" }) }
scope :type2, -> { joins(:types).where(types: { name: "Type2" }) }
end
Когда я пытаюсь использовать одну область (например, Product.type1), все идет хорошо, но две области одновременно (Product.type1.type2) возвращают пустой запрос. Да, один продукт может иметь несколько типов.
Конечной целью является создание фильтра товаров по типу с формой флажков. Когда я проверяю type1 и type2, я хочу видеть все мои продукты, которые имеют Type1 и Type1 одновременно.
UPD 1
Поэтому я попытался сделать несколько запросов, а затем и их, как предложил @ aaron.v. Я хотел сделать логику внутри функции так:
def products_by_type_name(types)
all_types = types.each { |type| Product.joins(:types).where(types: { name: type }).distinct }
...
end
Моя цель состояла в том, чтобы пройтись по каждому типу, собрать все продукты и затем & их внутри функции. Проблема в том, что когда я выполняю итерацию, каждый цикл возвращает строку вместо массива хэшей.
Product.joins(:types).where(types: { name: types }).distinct # returns array of hashes, it's okay.
types.each { |type| Product.joins(:types).where(types: { name: type }).distinct } # each loop returns string (Type name) instead of array of hashes.
Что я делаю неправильно?
РЕШЕНИЕ 1
Предложено @aaron.v, объяснено ниже
def self.by_type_name(types)
product_ids = []
types.each do |t|
product_ids << (joins(:types).where(types: { name: t }).distinct.select(:id).map(&:id))
end
find(product_ids.inject(:&))
end
РЕШЕНИЕ 2
Нашел на реддит. В этой функции вы выбираете все продукты по крайней мере с одним требуемым типом, а затем группируете только те, которые имеют необходимое количество типов. Таким образом, вы получаете только те продукты, которые принадлежат каждому типу одновременно.
def self.by_type_name(types)
joins(:types).where(types: { name: types }).distinct.group('products.id').having('count(*) = ?', types.each.count)
end
1 ответ
Если у вас есть опыт работы с базами данных, было бы совершенно очевидно, почему вы не сможете найти продукты с несколькими типами на основе того, как вы пишете свои области.
Запросы к базе данных, написанные из этих областей, будут умножать строки, чтобы у продукта, который имеет много типов, была отдельная строка для каждого типа. Ваш запрос при объединении обеих областей пишет
where `types`.name = "Type1" AND `types`.name = "Type2"
Этого никогда не произойдет, так как столбцы не добавляются с кратными одной и той же строки в соединении.
То, что вы хотите сделать, это иметь метод, который вы можете передать массив имен типов, чтобы найти его
def self.by_type_name(types)
joins(:types).where(types: { name: types }).distinct
end
Этот метод может принимать одно имя или массив имен для типов, которые вы хотите найти
Если вы передадите массив имен типов, таких как ["type1", "type2"], это приведет к запросу типа
where `types`.name in ("type1", "type2")
И тогда вы должны увидеть ожидаемые результаты
ОБНОВИТЬ
Чтобы пересмотреть то, что вам нужно в поиске продуктов, которые имеют все типы, заданные для вашего метода, я решил сделать это
def self.by_type_name(types)
product_ids = []
types.each do |t|
product_ids << (joins(:types).where(types: { name: t }).distinct.select(:id).map(&:id))
end
find(product_ids.inject(:&))
end
Этот метод требует массив имен типов (даже если это один тип, передайте массив с одним типом в нем). Для этого нужно найти все продукты с одним конкретным именем типа, выбрать только идентификатор продукта (который будет легче в вашей БД) и отобразить его в массив, который будет выгружен в массив product_ids. после зацикливания каждого имени типа он найдет все продукты, которые пересекаются в каждом из массивов, в результате чего будут получены продукты, в которые переданы все типы.