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

У меня есть две модели, 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 ]

1 голос
/ 14 июля 2010

Похоже, что в 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}")
}
1 голос
/ 14 июля 2010

Существует два распространенных решения в 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.

...