Найдите все записи, у которых количество ассоциаций больше нуля


98

Я пытаюсь сделать что-то, что, как я думал, будет простым, но, похоже, это не так.

У меня есть модель проекта, в которой много вакансий.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Я хочу получить все проекты, в которых есть хотя бы 1 вакансия. Я пробовал примерно так:

Project.joins(:vacancies).where('count(vacancies) > 0')

но он говорит

SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0).

Ответы:


66

joinsпо умолчанию использует внутреннее соединение, поэтому при использовании Project.joins(:vacancies)будут возвращены только те проекты, которые имеют связанную вакансию.

ОБНОВИТЬ:

Как указывает @mackskatz в комментарии, без groupпредложения приведенный выше код будет возвращать повторяющиеся проекты для проектов с более чем одной вакансией. Чтобы удалить дубликаты, используйте

Project.joins(:vacancies).group('projects.id')

ОБНОВИТЬ:

Как указано @Tolsee, вы также можете использовать distinct.

Project.joins(:vacancies).distinct

Например

[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""

1
Однако без применения предложения group by это вернет несколько объектов Project для проектов, которые имеют более одной вакансии.
— mackshkatz

1
Однако не генерирует эффективный оператор SQL.
— Дэвид Олдридж

Что ж, это Rails для вас. Если вы можете предоставить ответ sql (и объяснить, почему это неэффективно), это может быть намного полезнее.
— jvnill

О чем ты думаешь Project.joins(:vacancies).distinct?
— Tolsee

1
Это @Tolsee, кстати: D
— Толси

168

1) Чтобы получить проекты с минимум 1 вакансией:

Project.joins(:vacancies).group('projects.id')

2) Чтобы получить проекты с более чем 1 вакансией:

Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')

3) Или, если Vacancyмодель устанавливает кеш счетчика:

belongs_to :project, counter_cache: true

тогда это тоже будет работать:

Project.where('vacancies_count > ?', 1)

Правило перегиба для vacancyможет потребоваться указать вручную ?


2
Разве этого не должно быть Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Запрос количества вакансий вместо идентификаторов проектов
— Кейт Маттикс

1
Нет, @KeithMattix, не должно быть. Однако может быть, если вам так будет лучше; это вопрос предпочтений. Подсчет может производиться с любым полем в объединяемой таблице, для которого гарантированно будет значение в каждой строке. Большинство значимых кандидатов projects.id, project_idи vacancies.id. Я выбрал подсчет, project_idпотому что это поле, в котором выполняется соединение; позвоночник соединения, если хотите. Это также напоминает мне, что это соединительный стол.
— Арта

38

Ага, vacanciesэто не поле в объединении. Я считаю, что вы хотите:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")

16
# None
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')

5

Выполнение внутреннего соединения с таблицей has_many в сочетании с groupили uniqпотенциально очень неэффективно, и в SQL это было бы лучше реализовать как полусоединение, которое используется EXISTSс коррелированным подзапросом.

Это позволяет оптимизатору запросов проверять таблицу вакансий, чтобы проверить наличие строки с правильным project_id. Неважно, есть ли одна строка или миллион с этим project_id.

Это не так просто в Rails, но может быть достигнуто с помощью:

Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)

Аналогичным образом найдите все проекты, в которых нет вакансий:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)

Изменить: в последних версиях Rails вы получаете предупреждение об устаревании, говорящее вам не полагаться на existsделегирование в arel. Исправьте это с помощью:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)

Изменить: если вам неудобно использовать необработанный SQL, попробуйте:

Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)

Вы можете сделать это менее беспорядочным, добавив методы класса, чтобы скрыть использование arel_table, например:

class Project
  def self.id_column
    arel_table[:id]
  end
end

... так ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)

эти два предложения, похоже, не работают ... подзапрос Vacancy.where("vacancies.project_id = projects.id").exists?дает либо trueили false. Project.where(true)это ArgumentError.
— Les Nightingill

Vacancy.where("vacancies.project_id = projects.id").exists?не будет выполняться - это вызовет ошибку, потому что projectsотношение не будет существовать в запросе (и в приведенном выше примере кода также нет знака вопроса). Поэтому разложение этого на два выражения недопустимо и не работает. В последнее время Rails Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)выдает предупреждение об устаревании ... Я обновлю вопрос.
— Дэвид Олдридж

4

В Rails 4+, вы можете также использовать включаете или eager_load , чтобы получить тот же ответ:

Project.includes(:vacancies).references(:vacancies).
        where.not(vacancies: {id: nil})

Project.eager_load(:vacancies).where.not(vacancies: {id: nil})

4

Думаю, есть более простое решение:

Project.joins(:vacancies).distinct

1
Также можно использовать «отдельный», например Project.joins (: vacancies) .distinct
— Metaphysiker

Ты прав! Лучше использовать #distinct вместо #uniq. #uniq загрузит все объекты в память, но #distinct выполнит вычисления на стороне базы данных.
— Юрий Карпович

3

Без особой магии Rails вы можете:

Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')

Этот тип условий будет работать во всех версиях Rails, поскольку большая часть работы выполняется непосредственно на стороне БД. Кроме того, .countметод цепочки тоже подойдет. Как и раньше, меня обжигали вопросы Project.joins(:vacancies). Конечно, есть плюсы и минусы, поскольку он не зависит от БД.


1
Это намного медленнее, чем метод соединения и группировки, поскольку подзапрос select count (*) .. будет выполняться для каждого проекта.
— YasirAzgar 02

@YasirAzgar Метод объединения и группировки медленнее, чем метод «существует», потому что он все равно будет обращаться ко всем дочерним строкам, даже если их миллион.
— Дэвид Олдридж

0

Кроме того, можно использовать EXISTSс , SELECT 1а не выбирать все столбцы из vacanciesтаблицы:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")

-6

Ошибка говорит вам, что вакансии - это не столбец в проектах.

Это должно работать

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')

7
aggregate functions are not allowed in WHERE
— Камил Лелонек
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.