Vuoi trovare i record senza record associati in Rails 3

Considera una semplice associazione …

class Person has_many :friends end class Friend belongs_to :person end 

Qual è il modo più pulito per ottenere tutte le persone che NON hanno amici in ARel e / o meta_where?

E che dire di has_many: attraverso la versione

 class Person has_many :contacts has_many :friends, :through => :contacts, :uniq => true end class Friend has_many :contacts has_many :people, :through => :contacts, :uniq => true end class Contact belongs_to :friend belongs_to :person end 

Non voglio davvero usare counter_cache – e io da quello che ho letto non funziona con has_many: through

Non voglio estrarre tutti i record di person.friends e visualizzarli in loop in Ruby – Voglio avere una query / scope che posso usare con la gem meta_search

Non mi interessa il costo delle prestazioni delle query

E più lontano dal vero SQL, meglio è …

Questo è ancora abbastanza vicino a SQL, ma nel primo caso dovrebbe coinvolgere tutti senza amici:

 Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)') 

Meglio:

 Person.includes(:friends).where( :friends => { :person_id => nil } ) 

Per l’hmt è praticamente la stessa cosa, ti affidi al fatto che una persona senza amici non avrà nemmeno contatti:

 Person.includes(:contacts).where( :contacts => { :person_id => nil } ) 

Aggiornare

Hai una domanda su has_one nei commenti, quindi aggiorna semplicemente. Il trucco qui è che includes() aspetta il nome dell’associazione ma il where aspetta il nome della tabella. Per un has_one l’associazione sarà generalmente espressa al singolare, in modo che cambi, ma la parte where() rimarrà has_one . Quindi se una Person solo has_one :contact tua affermazione sarà:

 Person.includes(:contact).where( :contacts => { :person_id => nil } ) 

Aggiornamento 2

Qualcuno ha chiesto dell’inverso, amici senza persone. Come ho commentato di seguito, questo mi ha fatto capire che l’ultimo campo (sopra: il :person_id ) in realtà non deve essere correlato al modello che stai restituendo, deve solo essere un campo nella tabella di join. Saranno tutti nil quindi può essere nessuno di loro. Ciò porta a una soluzione più semplice a quanto sopra:

 Person.includes(:contacts).where( :contacts => { :id => nil } ) 

E poi passare a questo modo per rendere gli amici senza persone diventa ancora più semplice, si cambia solo la class in primo piano:

 Friend.includes(:contacts).where( :contacts => { :id => nil } ) 

Aggiornamento 3 – Rails 5

Grazie a @Anson per l’eccellente soluzione Rails 5 (dargli alcuni +1 per la sua risposta di seguito), puoi utilizzare left_outer_joins per evitare di caricare l’associazione:

 Person.left_outer_joins(:contacts).where( contacts: { id: nil } ) 

L’ho incluso qui così la gente lo troverà, ma per questo merita i +1. Grande aggiunta!

smathy ha una buona risposta di Rails 3.

Per Rails 5 , puoi usare left_outer_joins per evitare di caricare l’associazione.

 Person.left_outer_joins(:contacts).where( contacts: { id: nil } ) 

Guarda i documenti API . È stato introdotto nella richiesta pull # 12071 .

Persone che non hanno amici

 Person.includes(:friends).where("friends.person_id IS NULL") 

O che ha almeno un amico

 Person.includes(:friends).where("friends.person_id IS NOT NULL") 

Puoi farlo con Arel impostando gli ambiti su Friend

 class Friend belongs_to :person scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) } scope :to_nobody, ->{ where arel_table[:person_id].eq(nil) } end 

E poi, persone che hanno almeno un amico:

 Person.includes(:friends).merge(Friend.to_somebody) 

L’amico:

 Person.includes(:friends).merge(Friend.to_nobody) 

Entrambe le risposte di Dmarkow e Unixmonkey mi procurano ciò di cui ho bisogno – Grazie!

Ho provato entrambi nella mia vera app e ho avuto i tempi per loro – Ecco i due scopi:

 class Person has_many :contacts has_many :friends, :through => :contacts, :uniq => true scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") } scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") } end 

Ha funzionato con una vera app – una piccola tabella con circa 700 record “Person” – in media 5 run

Approccio di Unixmonkey ( :without_friends_v1 ) 813ms / query

approccio di dmarkow ( :without_friends_v2 ) 891ms / query (~ 10% più lento)

Ma poi mi è venuto in mente che non ho bisogno della chiamata a DISTINCT()... Sto cercando i record delle Person senza Contacts – quindi devono solo NOT IN essere nella lista delle persone di contatto. Così ho provato questo scopo:

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") } 

Questo ottiene lo stesso risultato ma con una media di 425 ms / chiamata – quasi la metà del tempo …

Ora potresti aver bisogno del DISTINCT in altre query simili – ma per quanto mi riguarda sembra funzionare bene.

Grazie per l’aiuto

Sfortunatamente, probabilmente stai considerando una soluzione che coinvolge SQL, ma potresti impostarla in un ambito e quindi utilizzare solo tale ambito:

 class Person has_many :contacts has_many :friends, :through => :contacts, :uniq => true scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0") end 

Quindi per ottenerli, puoi semplicemente fare Person.without_friends , e puoi anche concatenarlo con altri metodi di Arel: Person.without_friends.order("name").limit(10)

Una sottoquery correlata a NOT EXISTS dovrebbe essere veloce, in particolare quando aumenta il conteggio delle righe e il rapporto tra i record da figlio a genitore.

 scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)") 

Inoltre, per filtrare da un amico per esempio:

 Friend.where.not(id: other_friend.friends.pluck(:id))