Vuoi trovare record con nessun record associati a Rails
-
24-10-2019 - |
Domanda
Si consideri 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 poi che dire di un has_many: fino alla 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
Io davvero non voglio usare counter_cache - e da quello che ho letto non funziona con has_many: attraverso
Non voglio tirare tutti i record person.friends e ciclo attraverso di loro in Ruby - voglio avere una query / ambito che posso usare con la gemma meta_search
non mi dispiace il costo delle prestazioni delle query
E il più lontano da SQL effettiva meglio ...
Soluzione
Questo è ancora abbastanza vicino a SQL, ma dovrebbe ottenere tutti senza amici nel primo caso:
Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Altri suggerimenti
Meglio:
Person.includes(:friends).where( :friends => { :person_id => nil } )
Per la HMT è fondamentalmente la stessa cosa, è fare affidamento sul fatto che una persona che non ha amici anche non avere contatti:
Person.includes(:contacts).where( :contacts => { :person_id => nil } )
Aggiorna
Hai una domanda su has_one
nei commenti, così appena l'aggiornamento. Il trucco è che includes()
si aspetta che il nome dell'associazione, ma la where
aspetta che il nome della tabella. Per un has_one
sezione sarà generalmente espressa al singolare, in modo che i cambiamenti, ma i soggiorni parte where()
come è. Quindi, se un Person
has_one :contact
solo allora la sua dichiarazione sarebbe:
Person.includes(:contact).where( :contacts => { :person_id => nil } )
Aggiornamento 2
Qualcuno ha chiesto l'inverso, gli amici senza persone. Come ho commentato qui di seguito, questa realtà mi ha fatto capire che l'ultimo campo (sopra: la :person_id
) in realtà non deve essere collegato con il modello si sta tornando, deve solo essere un campo nella tabella join. Sono tutti andando essere nil
in modo che possa essere uno qualsiasi di loro. Questo porta a una soluzione più semplice a quanto sopra:
Person.includes(:contacts).where( :contacts => { :id => nil } )
E poi passare questo per tornare amici senza persone diventa ancora più semplice, si cambia solo la classe nella parte anteriore:
Friend.includes(:contacts).where( :contacts => { :id => nil } )
Aggiornamento 3 - Rails 5
Grazie a @Anson per l'eccellente Rails 5 soluzione (dargli qualche 1s + per la sua risposta qui sotto), è possibile utilizzare left_outer_joins
per evitare di caricare l'associazione:
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Ho incluso qui così la gente lo troverà, ma si merita le 1s + per questo. Grande aggiunta!
smathy ha una buona Rails 3 risposta.
Per Rails 5 , è possibile utilizzare left_outer_joins
per evitare di caricare l'associazione.
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Controlla la Documentazione API . E 'stato introdotto in richiesta di pull # 12071 .
Le persone che non hanno amici
Person.includes(:friends).where("friends.person_id IS NULL")
o che hanno almeno un amico
Person.includes(:friends).where("friends.person_id IS NOT NULL")
Si può fare questo con Arel attraverso la creazione di 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, le persone che hanno almeno un amico:
Person.includes(:friends).merge(Friend.to_somebody)
Il senza amici:
Person.includes(:friends).merge(Friend.to_nobody)
Entrambe le risposte da dmarkow e Unixmonkey ottenere me quello che mi serve - grazie!
Ho provato sia nella mia applicazione reale e tempi ottenuto per loro - Qui ci sono 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
Ran questo con un vero e proprio app - tavolino con ~ 700 record 'persona' - media di 5 piste
L'approccio di Unixmonkey (:without_friends_v1
) 813ms / interrogazione
di dmarkow approccio (:without_friends_v2
) 891ms / query (~ 10% più lento)
Ma poi mi venne in mente che non ho bisogno la chiamata a DISTINCT()...
Sto cercando record Person
con NO Contacts
- così hanno solo bisogno di essere NOT IN
l'elenco dei contatti person_ids
. Così ho provato questo scopo:
scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }
che ottiene lo stesso risultato, ma con una media di 425 ms / chiamata - quasi la metà del tempo ...
Ora potrebbe essere necessario il DISTINCT
in altre query simili -. Ma per il mio caso questo sembra funzionare bene
Grazie per il vostro aiuto
Purtroppo, probabilmente stai guardando una soluzione che comprenda SQL, ma è possibile impostare in un ambito e poi basta usare 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
Poi per farli, si può solo fare Person.without_friends
, ed è anche possibile concatenare questo con altri metodi di Arel: Person.without_friends.order("name").limit(10)
A NON ESISTE subquery correlata dovrebbe essere veloce, tanto più che il conteggio delle righe e il rapporto di bambino di record aumenta genitore.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
Inoltre, per filtrare da uno amico, per esempio:
Friend.where.not(id: other_friend.friends.pluck(:id))