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

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).

jphorta
источник

Ответы:

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\""
jvnill
источник
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')
wkhatch
источник
7
aggregate functions are not allowed in WHERE
Камил Лелонек