Trova tutti i record che hanno un conteggio di un'associazione maggiore di zero


98

Sto cercando di fare qualcosa che pensavo sarebbe stato semplice ma sembra che non lo sia.

Ho un modello di progetto che ha molti posti vacanti.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Voglio ottenere tutti i progetti che hanno almeno 1 posto vacante. Ho provato qualcosa di simile:

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

ma dice

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

Risposte:


65

joinsutilizza un join interno per impostazione predefinita, quindi l'utilizzo Project.joins(:vacancies)restituirà in effetti solo i progetti a cui è associato un posto vacante.

AGGIORNARE:

Come sottolineato da @mackskatz nel commento, senza una groupclausola, il codice sopra restituirà progetti duplicati per progetti con più di un posto vacante. Per rimuovere i duplicati, utilizzare

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

AGGIORNARE:

Come sottolineato da @Tolsee, puoi anche usare distinct.

Project.joins(:vacancies).distinct

Come esempio

[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
Tuttavia, senza applicare una clausola group by, ciò restituirebbe più oggetti Project per i progetti che hanno più di un posto vacante.
mackshkatz

1
Tuttavia, non genera un'istruzione SQL efficiente.
David Aldridge

Bene, questo è Rails per te. Se puoi fornire una risposta sql (e spiegare perché non è efficiente), potrebbe essere molto più utile.
jvnill

Cosa ne pensi Project.joins(:vacancies).distinct?
Tolsee

1
È @Tolsee btw: D
Tolsee

167

1) Per ottenere progetti con almeno 1 posto vacante:

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

2) Per ottenere progetti con più di 1 posto vacante:

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

3) Oppure, se il Vacancymodello imposta la cache del contatore:

belongs_to :project, counter_cache: true

allora funzionerà anche questo:

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

vacancyPotrebbe essere necessario specificare manualmente la regola di flessione ?


2
Non dovrebbe essere questo Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Interrogazione del numero di posti vacanti invece degli ID progetto
Keith Mattix

No, @KeithMattix, non dovrebbe essere. Esso può essere, tuttavia, se si legge meglio a voi; è una questione di preferenza. Il conteggio può essere eseguito con qualsiasi campo nella tabella di join a cui è garantito un valore in ogni riga. La maggior parte dei candidati sono significative projects.id, project_ide vacancies.id. Ho scelto di contare project_idperché è il campo su cui si fa l'unione; la spina dorsale dell'unione se vuoi. Mi ricorda anche che questa è una tabella di join.
Arta

36

Sì, vacanciesnon è un campo nel join. Credo tu voglia:

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

L'esecuzione di un join interno alla tabella has_many combinata con un groupo uniqè potenzialmente molto inefficiente e in SQL sarebbe meglio implementata come semi join che utilizza EXISTScon una sottoquery correlata.

Ciò consente all'ottimizzatore di query di sondare la tabella dei posti vacanti per verificare l'esistenza di una riga con il project_id corretto. Non importa se c'è una riga o un milione che hanno quel project_id.

Non è così semplice in Rails, ma può essere ottenuto con:

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

Allo stesso modo, trova tutti i progetti che non hanno posti vacanti:

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

Modifica: nelle versioni recenti di Rails viene visualizzato un avviso di deprecazione che ti dice di non fare affidamento existssull'essere delegato ad arel. Risolvi il problema con:

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

Modifica: se ti senti a disagio con l'SQL grezzo, prova:

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

Puoi renderlo meno disordinato aggiungendo metodi di classe per nascondere l'uso di arel_table, ad esempio:

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

... così ...

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

questi due suggerimenti non sembrano funzionare ... la sottoquery Vacancy.where("vacancies.project_id = projects.id").exists?restituisce o trueo false. Project.where(true)è un ArgumentError.
Les Nightingill

Vacancy.where("vacancies.project_id = projects.id").exists?non verrà eseguito - solleverà un errore perché la projectsrelazione non esiste nella query (e non c'è nemmeno un punto interrogativo nel codice di esempio sopra). Quindi scomporre questo in due espressioni non è valido e non funziona. Recentemente Rails Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)solleva un avviso di deprecazione ... aggiornerò la domanda.
David Aldridge

4

In Rails 4+, puoi anche utilizzare include o eager_load per ottenere la stessa risposta:

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

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

4

Penso che ci sia una soluzione più semplice:

Project.joins(:vacancies).distinct

1
È anche possibile utilizzare "distinto", ad esempio Project.joins (: posti vacanti) .distinct
Metaphysiker

Hai ragione! È meglio usare #distinct invece di #uniq. #uniq caricherà tutti gli oggetti in memoria, ma #distinct farà i calcoli dal lato del database.
Yuri Karpovich

3

Senza molta magia di Rails, puoi fare:

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

Questo tipo di condizioni funzionerà in tutte le versioni di Rails poiché gran parte del lavoro viene svolto direttamente sul lato DB. Inoltre, anche il .countmetodo di concatenamento funzionerà bene. Sono stato bruciato da domande come Project.joins(:vacancies)prima. Naturalmente, ci sono pro e contro in quanto non è indipendente dal DB.


1
Questo è molto più lento del metodo join e group, poiché la sottoquery 'select count (*) ..' verrà eseguita per ogni progetto.
YasirAzgar

@YasirAzgar Il metodo join and group è più lento del metodo "exist" perché accederà comunque a tutte le righe figlie, anche se ce ne sono un milione.
David Aldridge

0

Puoi anche utilizzare EXISTScon SELECT 1anziché selezionare tutte le colonne dalla vacanciestabella:

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

-6

L'errore ti sta dicendo che i posti vacanti non sono una colonna nei progetti, in fondo.

Questo dovrebbe funzionare

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

7
aggregate functions are not allowed in WHERE
Kamil Lelonek
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.