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

È stato utile?

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))
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top