Znajdź wszystkie rekordy, których liczba asocjacji jest większa niż zero

98

Próbuję zrobić coś, co wydawało mi się proste, ale wydaje się, że tak nie jest.

Mam model projektu z wieloma wakatami.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Chcę otrzymać wszystkie projekty, które mają co najmniej 1 wakat. Próbowałem czegoś takiego:

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

ale mówi

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
źródło

Odpowiedzi:

66

joinsużywa domyślnie sprzężenia wewnętrznego, więc użycie Project.joins(:vacancies)spowoduje w efekcie zwrócenie tylko projektów z powiązanym wakatem.

AKTUALIZACJA:

Jak wskazał @mackskatz w komentarzu, bez groupklauzuli, powyższy kod zwróci zduplikowane projekty dla projektów z więcej niż jednym wakatem. Aby usunąć duplikaty, użyj

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

AKTUALIZACJA:

Jak wskazał @Tolsee, możesz również użyć distinct.

Project.joins(:vacancies).distinct

Jako przykład

[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
źródło
1
Jednak bez zastosowania klauzuli group by zwróciłoby to wiele obiektów projektu dla projektów, które mają więcej niż jeden wakat.
mackshkatz
1
Nie generuje jednak wydajnej instrukcji SQL.
David Aldridge
Cóż, to jest Rails dla Ciebie. Jeśli możesz podać odpowiedź sql (i wyjaśnić, dlaczego nie jest to wydajne), może to być o wiele bardziej pomocne.
jvnill
Co sądzisz o Project.joins(:vacancies).distinct?
Tolsee
1
To @Tolsee btw: D
Tolsee
168

1) Aby otrzymać projekty z co najmniej 1 wakatem:

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

2) Aby otrzymać projekty z więcej niż 1 wakatem:

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

3) Lub, jeśli Vacancymodel ustawia pamięć podręczną liczników:

belongs_to :project, counter_cache: true

to też zadziała:

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

vacancyMoże trzeba określić regułę fleksji dla ręcznie ?

Arta
źródło
2
Nie powinno tak być Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Zapytanie o liczbę wolnych miejsc pracy zamiast identyfikatorów projektów
Keith Mattix
1
Nie, @KeithMattix, nie powinno . To może być jednak, jeśli to brzmi lepiej dla ciebie; to kwestia preferencji. Liczenie można wykonać z dowolnym polem w tabeli łączenia, które ma gwarantowaną wartość w każdym wierszu. Większość znaczących kandydatów projects.id, project_idoraz vacancies.id. Zdecydowałem się liczyć, project_idponieważ jest to pole, na którym jest wykonywane łączenie; kręgosłup połączenia, jeśli chcesz. Przypomina mi również, że jest to stół do wspólnego stołu.
Arta
37

Tak, vacanciesnie jest polem w złączeniu. Wierzę, że chcesz:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
Peter Alfvin
źródło
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')
dorycki
źródło
5

Wykonywanie sprzężenia wewnętrznego do tabeli has_many w połączeniu z grouplub uniqjest potencjalnie bardzo nieefektywne, aw SQL byłoby lepiej zaimplementowane jako półzłączenie, które używa EXISTSskorelowanego podzapytania.

Dzięki temu optymalizator zapytań może sondować tabelę wakatów w celu sprawdzenia, czy istnieje wiersz z poprawnym project_id. Nie ma znaczenia, czy istnieje jeden wiersz, czy milion, które mają ten identyfikator projektu.

Nie jest to takie proste w Railsach, ale można to osiągnąć za pomocą:

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

Podobnie znajdź wszystkie projekty, w których nie ma wolnych miejsc pracy:

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

Edycja: w ostatnich wersjach Railsów otrzymujesz ostrzeżenie o wycofaniu, informujące, że nie możesz polegać na existsdelegowaniu na arel. Napraw to za pomocą:

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

Edycja: jeśli nie czujesz się komfortowo z surowym SQL, spróbuj:

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

Możesz zrobić to mniej bałaganem, dodając metody klasowe, aby ukryć użycie arel_table, na przykład:

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

... więc ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)
David Aldridge
źródło
te dwie sugestie wydają się nie działać ... podzapytanie Vacancy.where("vacancies.project_id = projects.id").exists?daje albo truealbo false. Project.where(true)jest ArgumentError.
Les Nightingill
Vacancy.where("vacancies.project_id = projects.id").exists?nie zostanie wykonany - spowoduje to zgłoszenie błędu, ponieważ projectsrelacja nie będzie istniała w zapytaniu (aw przykładowym kodzie powyżej również nie ma znaku zapytania). Więc rozkładanie tego na dwa wyrażenia nie jest poprawne i nie działa. W ostatnich Railsach Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)pojawia się ostrzeżenie o wycofaniu ... Zaktualizuję pytanie.
David Aldridge
4

W Railsach 4+ możesz także użyć include lub eager_load, aby uzyskać tę samą odpowiedź:

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

Project.eager_load(:vacancies).where.not(vacancies: {id: nil})
konyak
źródło
4

Myślę, że jest prostsze rozwiązanie:

Project.joins(:vacancies).distinct
Jurij Karpowicz
źródło
1
Możliwe jest również użycie „odrębnych”, np. Project.joins (: vacancies) .distinct
Metaphysiker
Masz rację! Lepiej jest użyć #distinct zamiast #uniq. #uniq załaduje wszystkie obiekty do pamięci, ale #distinct wykona obliczenia po stronie bazy danych.
Yuri Karpovich
3

Bez dużej magii Railsów możesz:

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

Tego typu warunki będą działać we wszystkich wersjach Railsów, ponieważ większość pracy jest wykonywana bezpośrednio po stronie DB. Poza tym .countmetoda łączenia w łańcuch też będzie działać dobrze. Wypalały mnie zapytania, jak Project.joins(:vacancies)wcześniej. Oczywiście są plusy i minusy, ponieważ nie jest agnostykiem DB.

konyak
źródło
1
Jest to znacznie wolniejsze niż metoda łączenia i grupowania, ponieważ podzapytanie „select count (*) ..” zostanie wykonane dla każdego projektu.
YasirAzgar
@YasirAzgar Metoda łączenia i grupowania jest wolniejsza niż metoda „istnieje”, ponieważ nadal będzie miała dostęp do wszystkich wierszy podrzędnych, nawet jeśli jest ich milion.
David Aldridge
0

Możesz również użyć EXISTSz SELECT 1zamiast wybierania wszystkich kolumn w vacanciestabeli:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")
KM Rakibul Islam
źródło
-6

Błąd mówi ci, że wakaty nie są w zasadzie kolumną w projektach.

To powinno działać

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')
wkhatch
źródło
7
aggregate functions are not allowed in WHERE
Kamil Lelonek