Знайдіть усі записи, у яких кількість зв’язків більша за нуль


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

Відповіді:


65

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
Однак без застосування групи за умовою це поверне декілька об'єктів Project для проектів, які мають більше однієї вакансії.
mackshkatz

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

Ну, це Рейки для вас. Якщо ви можете надати sql відповідь (і пояснити, чому це не ефективно), це може бути набагато корисніше.
jvnill

Про що ти думаєш Project.joins(:vacancies).distinct?
Толсі

1
Це @Tolsee btw: D
Толсі

167

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')? Запит щодо кількості вакансій замість ідентифікаторів проекту
Кіт Маттікс

Ні, @KeithMattix, так не повинно бути. Однак це може бути, якщо він читає вам краще; це питання переваг. Підрахунок можна здійснити з будь-якого поля таблиці з’єднання, яке гарантовано має значення в кожному рядку. Більшість значущих кандидатів projects.id, project_idі vacancies.id. Я вирішив рахувати, project_idтому що це поле, на якому здійснюється з'єднання; хребта з’єднання, якщо хочете. Це також нагадує мені, що це таблиця приєднання.
Арта

36

Так, 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. Не має значення, чи є один ряд чи мільйон, які мають цей проект_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 (: вакансії) .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.03.18

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