Как написать именованную область видимости для фильтрации по всему массиву, переданному в, а не только путем сопоставления одного элемента (используя IN)

У меня есть две модели, Project и Category, между которыми есть отношения многие ко многим. Модель проекта очень проста:

class Project < ActiveRecord::Base
  has_and_belongs_to_many :categories

  scope :in_categories, lambda { |categories|
    joins(:categories).
    where("categories.id in (?)", categories.collect(&:to_i))
  }
end

Область действия:in_categories принимает массив идентификаторов категорий (в виде строк), поэтому с помощью этой области я могу получить обратно каждый проект, который принадлежит хотя бы к одной из переданных категорий.

Но то, что я на самом деле пытаюсь сделать, это фильтр (лучшее название было бы:has_categories). Я хочу просто получить проекты, которые принадлежат всем переданным категориям. Поэтому, если я передам ["1", "3", "4"], я хочу получить только проекты, которые принадлежат всем категориям.

2 ответа

Решение

В SQL есть два распространенных решения для того, что вы описываете.

Автообъединение:

SELECT ...
FROM Projects p
JOIN Categories c1 ON c1.project_id = p.id
JOIN Categories c3 ON c3.project_id = p.id
JOIN Categories c4 ON c4.project_id = p.id
WHERE (c1.id, c3.id, c4.id) = (1, 3, 4);

Обратите внимание, что я использую синтаксис для сравнения кортежей. Это эквивалентно:

WHERE c1.id = 1 AND c3.id = 3 AND c4.id = 4;

В целом, решение для самостоятельного объединения имеет очень хорошую производительность, если у вас есть индекс покрытия. Наверное Categories.(project_id,id) будет правильным индексом, но для уверенности проанализируйте SQL с помощью EXPLAIN.

Недостатком этого метода является то, что вам нужно четыре объединения, если вы ищете проекты, которые соответствуют четырем различным категориям. Пять объединений для пяти категорий и т. Д.

Группа по:

SELECT ...
FROM Projects p
JOIN Categories cc ON c.project_id = p.id
WHERE c.id IN (1, 3, 4)
GROUP BY p.id
HAVING COUNT(*) = 3;

Если вы используете MySQL (я полагаю, вы используете), большинство запросов GROUP BY вызывают временную таблицу, и это снижает производительность.

Я оставлю это в качестве упражнения для вас, чтобы адаптировать одно из этих решений SQL к эквивалентному Rails ActiveRecord API.

Кажется, что в ActiveRecord вы бы сделали это так:

scope :has_categories, lambda { |categories|
  joins(:categories).
  where("categories.id in (?)", categories.collect(&:to_i)).
  group("projects.id HAVING COUNT(projects.id) = #{categories.count}")
}
Другие вопросы по тегам