Rails'te ilişkili kaydı olmayan kayıtları bulmak istiyorum


178

Basit bir ilişki düşünün ...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

ARel'de ve / veya meta_yerde hiç arkadaşı olmayan herkese ulaşmanın en temiz yolu nedir?

Peki ya bir has_many: sürüm aracılığıyla

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

Ben gerçekten counter_cache kullanmak istemiyorum - ve ben okudum ne has_many ile çalışmaz: yoluyla

Tüm person.friends kayıtlarını çekmek ve Ruby'de onları döngü istemiyorum - meta_search gem ile kullanabileceğiniz bir sorgu / kapsam istiyorum

Sorguların performans maliyetine aldırmıyorum

Ve gerçek SQL'den ne kadar uzakta olursa o kadar iyi ...

Yanıtlar:


110

Bu hala SQL'e oldukça yakın, ancak ilk durumda hiç arkadaşı olmayan herkesi almalı:

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

6
Arkadaş tablosunda 10000000 kayıt olduğunu hayal et. Bu durumda performans ne olacak?
goodniceweb

@goodniceweb Yinelenen sıklığınıza bağlı olarak, büyük olasılıkla DISTINCT. Aksi takdirde, bu durumda verileri ve dizini normalleştirmek istediğinizi düşünüyorum. Bunu bir friend_idshstore veya serileştirilmiş sütun oluşturarak yapabilirim . Sonra söyleyebilirsinPerson.where(friend_ids: nil)
Unixmonkey

Eğer sql kullanacaksanız, muhtemelen kullanmak daha iyidir not exists (select person_id from friends where person_id = person.id)(Veya belki de, people.idya da persons.idtablonuzun ne olduğuna bağlı olarak.) Belirli bir durumda en hızlı olanın ne olduğundan emin değilim, ama geçmişte bu benim için iyi çalıştı ActiveRecord'u kullanmaya çalışmıyordu.
2018'de

442

Daha iyi:

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

HMT için temelde aynı şey, arkadaşsız bir kişinin de hiçbir kişisinin olmayacağına güveniyorsunuz:

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

Güncelleme

has_oneYorumlar hakkında bir soru var , bu yüzden sadece güncelleme. Buradaki hile includes(), ilişkilendirmenin whereadını beklemektedir, ancak tablonun adını beklemektedir. Bir has_onedernek genellikle tekil olarak ifade edilir, böylece değişir, ama where()kısım olduğu gibi kalır. Yani eğer Personsadece bir has_one :contactifadeniz:

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

Güncelleme 2

Birisi tersini sordu, insansız arkadaşlar. Aşağıda yorumladığım gibi, bu aslında son alanın (yukarıda: the :person_id) gerçekten geri döndüğünüz modelle ilgili olması gerekmediğini, sadece birleştirme tablosunda bir alan olması gerektiğini fark etmemi sağladı . Hepsi öyle olacak, nilböylece herhangi biri olabilir. Bu, yukarıdakilere daha basit bir çözüm sağlar:

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

Ve daha sonra hiç kimseyle arkadaşlarınızı geri getirmek için bunu değiştirmek daha da kolaylaşıyor, sadece öndeki sınıfı değiştiriyorsunuz:

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

Güncelleme 3 - Raylar 5

Mükemmel Rails 5 çözümü için @ Johnson sayesinde (aşağıdaki cevabı için ona + 1'ler verin), left_outer_joinsilişkilendirmeyi yüklemekten kaçınmak için kullanabilirsiniz :

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

Buraya ekledim, böylece insanlar bulacak, ama bunun için + 1'leri hak ediyor. Harika bir ek!

Güncelleme 4 - Raylar 6.1

Yaklaşan 6.1'de bunu yapabileceğinizi işaret ettiği için Tim Park'a teşekkürler :

Person.where.missing(:contacts)

Sayesinde yazı o da bağlantılı.


4
Bunu daha temiz bir kapsama dahil edebilirsiniz.
Eytan

3
Çok daha iyi cevap, diğerinin neden kabul edildiğinden emin değilim.
Tamik Soziev

5
Evet öyle, has_onederneğiniz için tekil bir adınız olduğu varsayılarak, çağrıdaki ilişkilendirmenin adını değiştirmeniz gerekir includes. Yani has_one :contactiçinde olduğunu varsayarak Personkodunuz olurduPerson.includes(:contact).where( :contacts => { :person_id => nil } )
smathy

3
Friend modelinizde ( self.table_name = "custom_friends_table_name") özel bir tablo adı kullanıyorsanız , kullanın Person.includes(:friends).where(:custom_friends_table_name => {:id => nil}).
Zek

5
@smathy Rails 6.1'deki güzel bir güncelleme missingtam olarak bunu yapmak için bir yöntem ekliyor !
Tim Park

172

smathy iyi bir Rails 3 cevabı var.

Rails 5left_outer_joins için ilişkilendirmeyi yüklemekten kaçınmak için kullanabilirsiniz .

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

API belgelerine göz atın . 12071 numaralı çekme isteğinde tanıtıldı .


Bunun bir dezavantajı var mı? Kontrol ettim ve 0.1 ms daha hızlı yükledi .includes
Qwertie

İlişkilendirmeyi yüklememeniz, daha sonra gerçekten erişiyorsanız bunun bir dezavantajı, ancak ona erişmemenizin bir yararıdır. Sitelerim için 0,1 ms'lik bir isabet oldukça ihmal edilebilir, bu nedenle .includesyükleme süresinde ekstra maliyet optimizasyon konusunda çok endişeleneceğim bir şey olmaz. Kullanım durumunuz farklı olabilir.
Anson

1
Ve henüz Rails 5'iniz yoksa, bunu yapabilirsiniz: Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')Bir kapsam olarak da iyi çalışır. Bunu Rails projelerimde her zaman yaparım.
Frank

3
Bu yöntemin en büyük avantajı bellek tasarrufudur. Bunu yaptığınızda includes, tüm bu AR nesneleri belleğe yüklenir, bu da tablolar büyüdükçe kötü şeyler olabilir. Kişi kaydına erişmeniz left_outer_joinsgerekmiyorsa, kişi belleğe yüklenmez. SQL istek hızı aynı, ancak genel uygulama avantajı çok daha büyük.
chrismanderson

2
Bu gerçekten iyi! Teşekkürler! Eğer raylar tanrılar belki de basit olarak uygulayabilirlerse Person.where(contacts: nil)veya Person.with(contact: contact)'tecrübe' içine çok fazla tecavüzün olduğu yerlerde kullanılıyorsa - ancak bu temas göz önüne alındığında: zaten bir dernek olarak ayrıştırılıp tanımlanıyorsa, arel'in gerekenleri kolayca çözebileceği mantıklı görünüyor. ...
Justin Maxwell

14

Arkadaşı olmayanlar

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

Ya da en az bir arkadaşı var

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

Bunu Arel ile kapsam oluşturarak yapabilirsiniz. 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

Ve sonra, en az bir arkadaşı olan kişiler:

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

Arkadaşsız:

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

2
Ayrıca şunları da yapabilirsiniz: Person.includes (: arkadaşlar) .where (arkadaşlar: {person: nil})
ReggieB

1
Not: Birleştirme stratejisi bazen şöyle bir uyarı verebilirDEPRECATION WARNING: It looks like you are eager loading table(s) Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string
genkilabs

12

Hem dmarkow hem de Unixmonkey'in cevapları bana ihtiyacım olanı veriyor - Teşekkürler!

Benim gerçek app hem de denedim ve onlar için zamanlamaları var - İşte iki kapsamı:

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

Bu gerçek bir uygulama ile koştu - ~ 700 'Kişi' kayıtları ile küçük tablo - ortalama 5 çalışır

Unixmonkey yaklaşımı ( :without_friends_v1) 813 ms / sorgu

dmarkow'un yaklaşımı ( :without_friends_v2) 891 ms / sorgu (~% 10 daha yavaş)

Ama sonra bana NO ile kayıtları DISTINCT()...arıyorum çağrısına ihtiyacım olmadı - bu yüzden sadece temas listesi olması gerekiyor . Bu kapsamı denedim:PersonContactsNOT INperson_ids

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

Bu aynı sonucu alır, ancak ortalama 425 ms / çağrı ile - neredeyse yarısı ...

Şimdi DISTINCTdiğer benzer sorgularda ihtiyacınız olabilir - ama benim durumum için bu iyi çalışıyor gibi görünüyor.

Yardımınız için teşekkürler


5

Ne yazık ki, muhtemelen SQL içeren bir çözüme bakıyorsunuz, ancak bunu bir kapsamda ayarlayabilir ve daha sonra bu kapsamı kullanabilirsiniz:

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

Onları almak için, bunu yapabilir Person.without_friendsve bunu diğer Arel yöntemleriyle de zincirleyebilirsiniz:Person.without_friends.order("name").limit(10)


1

NOT EXISTS ile ilişkili bir alt sorgu, özellikle satır sayısı ve çocuğun ebeveyn kayıtlarına oranı arttıkça hızlı olmalıdır.

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

1

Ayrıca, örneğin bir arkadaşınız tarafından filtrelenmek için:

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

3
Bu, bir alt sorgu yerine 2 sorgu ile sonuçlanır.
grepsedawk
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.