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


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.