Chcesz znaleźć rekordy bez powiązanych rekordów w Rails 3

Rozważmy proste skojarzenie...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

Jaki jest najczystszy sposób, aby wszystkie osoby, które nie mają przyjaciół w ARel i / lub meta_where?

A potem co z has_many: through version

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

Naprawdę nie chcę używać counter_cache - I z tego co czytałem to nie działa z has_many: through

Nie chcę ciągnąć całej osoby.znajomi nagrywają i przeglądają je w Ruby - chcę mieć zapytanie / zakres, którego mogę użyć z meta_search gem

Nie przeszkadza mi koszt wykonania zapytań

I im dalej od rzeczywistego SQL tym lepiej...

Author: craic.com, 2011-03-16

8 answers

To jest nadal dość blisko SQL, ale powinien dostać każdy bez przyjaciół w pierwszym przypadku:

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
 87
Author: Unixmonkey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-16 15:22:42

Lepiej:

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

Dla hmt to w zasadzie to samo, polegasz na tym, że osoba bez przyjaciół również nie będzie miała kontaktów: {]}

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

Update

Mam pytanie o has_one w komentarzach, więc po prostu aktualizuję. Sztuczka polega na tym, że includes() oczekuje nazwy asocjacji, ale where oczekuje nazwy tabeli. Dla has_one Asocjacja będzie ogólnie wyrażona w liczbie pojedynczej, tak że się zmienia, ale where() część pozostaje taka, jaka jest. Więc jeśli Person tylko has_one :contact to Twoje stwierdzenie będzie:

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

Update 2

Ktoś pytał o odwrotność, przyjaciele bez ludzi. Jak komentowałem poniżej, uświadomiło mi to, że ostatnie pole (powyżej: :person_id) nie musi być związane z Modelem, który zwracasz, tylko musi być polem w tabeli join. Wszystkie będą nil, więc może to być każdy z nich. Prowadzi to do prostszego rozwiązania powyższego:

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

I wtedy zmiana tego na powrót przyjaciół bez ludzi staje się jeszcze prostsza, zmieniasz tylko klasę z przodu: {]}

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

Update 3-Rails 5

Dzięki @Anson za doskonałe rozwiązanie Rails 5 (daj mu kilka + 1 za jego odpowiedź poniżej), możesz użyć left_outer_joins, aby uniknąć ładowania skojarzenia:

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

Umieściłem go tutaj, aby ludzie go znaleźli, ale zasługuje na +1 za to. Świetny dodatek!

 360
Author: smathy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-07 00:54:24

Smathy ma dobrą odpowiedź Rails 3.

Dla Rails 5, możesz użyć left_outer_joins, aby uniknąć wczytywania asocjacji.

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

Sprawdź dokumenty api. Został wprowadzony w pull request #12071.

 102
Author: Anson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-09 16:19:01

Osoby, które nie mają znajomych

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

Lub które mają przynajmniej jednego przyjaciela

Person.includes(:friends).where("friends.person_id IS NOT NULL")
Możesz to zrobić za pomocą Arel, ustawiając zakresy na 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

A potem osoby, które mają przynajmniej jednego przyjaciela:

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

Bez przyjaciół:

Person.includes(:friends).merge(Friend.to_nobody)
 12
Author: novemberkilo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-29 16:05:09

Obie odpowiedzi od dmarkow i Unixmonkey dają mi to , czego potrzebuję-Dziękuję!

Wypróbowałem oba w mojej prawdziwej aplikacji i mam dla nich timingi - oto dwa zakresy:

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
W 2011 roku firma została założona przez firmę Garmin Sp. z o. o. z siedzibą w Warszawie, która od 2011 roku zajmuje się produkcją i dystrybucją sprzętu komputerowego.]}

Unixmonkey ' s approach (:without_friends_v1) 813ms / query

Dmarkow ' s approach (:without_friends_v2) 891ms / query (~ 10% wolniej)

Ale wtedy przyszło mi do głowy, że nie potrzebuję telefonu do DISTINCT()... jestem Szukam Person rekordów bez Contacts - więc muszą być NOT IN lista kontaktów person_ids. Więc wypróbowałem ten zakres:

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

To daje ten sam wynik, ale ze średnią 425 ms / połączenie - prawie połowę czasu...

Teraz możesz potrzebować DISTINCT w innych podobnych zapytaniach - ale w moim przypadku wydaje się to działać dobrze.

Dzięki za pomoc

 10
Author: craic.com,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-11 19:02:30

Niestety, prawdopodobnie patrzysz na rozwiązanie obejmujące SQL, ale możesz ustawić je w zakresie, a następnie po prostu użyć tego zakresu:

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

Następnie, aby je uzyskać, możesz po prostu zrobić Person.without_friends, i możesz również połączyć to z innymi metodami Arel: Person.without_friends.order("name").limit(10)

 5
Author: Dylan Markow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-16 00:29:54

A nie istnieje skorelowane zapytania podrzędne powinny być szybkie, zwłaszcza gdy liczba wierszy i stosunek rekordów potomnych do rekordów nadrzędnych wzrasta.

scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
 1
Author: David Aldridge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-16 08:40:28

Również, aby odfiltrować przez jednego znajomego na przykład:

Friend.where.not(id: other_friend.friends.pluck(:id))
 1
Author: Dorian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-01 23:53:13