रेल में कोई संबंधित रिकॉर्ड के साथ रिकॉर्ड खोजना चाहते हैं


178

एक साधारण संगति पर विचार करें ...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

उन सभी व्यक्तियों को प्राप्त करने का सबसे अच्छा तरीका क्या है जिनके ARel और / या meta_where में NO मित्र नहीं हैं?

और फिर has_many के बारे में क्या: संस्करण के माध्यम से

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

मैं वास्तव में counter_cache का उपयोग नहीं करना चाहता - और मैंने जो पढ़ा है वह has_many के साथ काम नहीं करता है: के माध्यम से

मैं रूबी में उनके माध्यम से सभी person.friends रिकॉर्ड्स और लूप को खींचना नहीं चाहता हूं - मैं एक क्वेरी / गुंजाइश चाहता हूं जिसे मैं meta_search मणि ​​के साथ उपयोग कर सकता हूं

मुझे प्रश्नों की प्रदर्शन लागत पर कोई आपत्ति नहीं है

और वास्तविक एसक्यूएल से दूर बेहतर ...

जवाबों:


110

यह अभी भी एसक्यूएल के बहुत करीब है, लेकिन इसे पहले मामले में बिना किसी मित्र के सभी को प्राप्त करना चाहिए:

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

6
ज़रा सोचिए दोस्तों टेबल में आपके 10000000 रिकॉर्ड हैं। उस मामले में प्रदर्शन के बारे में क्या?
गुडनिकेब डेब

@goodniceweb आपकी डुप्लिकेट आवृत्ति के आधार पर, आप शायद ड्रॉप कर सकते हैं DISTINCT। अन्यथा, मुझे लगता है कि आप उस स्थिति में डेटा और इंडेक्स को सामान्य करना चाहते हैं। मैं एक friend_idshstore या धारावाहिक कॉलम बनाकर ऐसा कर सकता हूं । तब आप कह सकते हैंPerson.where(friend_ids: nil)
यूनिक्समॉक्नेज़

यदि आप sql का उपयोग करने जा रहे हैं, तो आपकी तालिका के आधार पर not exists (select person_id from friends where person_id = person.id)(या शायद people.idया persons.id, के आधार पर) का उपयोग करना बेहतर है।) निश्चित नहीं है कि किसी विशेष स्थिति में सबसे तेज़ क्या है, लेकिन अतीत में यह मेरे लिए अच्छी तरह से काम किया है जब मैं ActiveRecord का उपयोग करने का प्रयास नहीं कर रहा था।
नाकाम करें

442

बेहतर:

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

एचएमटी के लिए यह मूल रूप से एक ही बात है, आप इस तथ्य पर भरोसा करते हैं कि बिना दोस्तों वाले व्यक्ति का कोई संपर्क नहीं होगा:

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

अपडेट करें

has_oneटिप्पणियों के बारे में एक सवाल है , तो बस अद्यतन कर रहा है। यहाँ चाल यह है कि includes()एसोसिएशन whereके नाम की उम्मीद है लेकिन तालिका के नाम की उम्मीद है। एक के लिए has_oneसंघ आम तौर पर एकवचन में व्यक्त किया जाएगा, ताकि परिवर्तन है, लेकिन where()यह है हिस्सा रहता है के रूप में। इसलिए यदि Personकेवल तभी has_one :contactआपका कथन होगा:

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

अपडेट २

किसी ने उलटे, बिना किसी लोगों के दोस्त के बारे में पूछा। जैसा कि मैंने नीचे टिप्पणी की है, यह वास्तव में मुझे एहसास हुआ कि अंतिम फ़ील्ड (ऊपर :person_id:) वास्तव में उस मॉडल से संबंधित नहीं है जिसे आप वापस कर रहे हैं, यह सिर्फ ज्वाइन टेबल में फ़ील्ड होना है। वे सभी होने जा रहे हैं nilतो यह उनमें से कोई भी हो सकता है। यह ऊपर के लिए एक सरल समाधान की ओर जाता है:

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

और फिर बिना किसी लोगों के साथ दोस्तों को वापस करने के लिए इसे स्विच करना और भी सरल हो जाता है, आप केवल सामने वाले वर्ग को बदलते हैं:

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

अपडेट 3 - रेल्स 5

उत्कृष्ट रेल 5 समाधान के लिए @ एसन के लिए धन्यवाद (नीचे उसके उत्तर के लिए उसे कुछ + 1s दें), आप left_outer_joinsसंघ को लोड करने से बचने के लिए उपयोग कर सकते हैं :

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

मैंने इसे यहां शामिल किया है ताकि लोग इसे पा सकें, लेकिन वह इसके लिए + 1s का हकदार है। बढ़िया जोड़!

अपडेट 4 - रेल्स 6.1

टिम पार्क की ओर इशारा करते हुए धन्यवाद कि आने वाले 6.1 में आप ऐसा कर सकते हैं:

Person.where.missing(:contacts)

उस पोस्ट के लिए धन्यवाद, जो उन्होंने भी लिंक की है।


4
आप इसे एक ऐसे दायरे में शामिल कर सकते हैं, जो ज्यादा साफ-सुथरा हो।
ईटान

3
सुदूर बेहतर उत्तर, निश्चित नहीं है कि अन्य को क्यों स्वीकार किया गया है।
तामिक सोजिएव

5
हां यह करता है, बस यह मानते हुए कि आपके पास अपने has_oneएसोसिएशन के लिए एक विलक्षण नाम है, आपको includesकॉल में एसोसिएशन का नाम बदलने की आवश्यकता है । तो यह मानते हुए कि यह has_one :contactअंदर था Personतो आपका कोड होगाPerson.includes(:contact).where( :contacts => { :person_id => nil } )
स्माइली

3
यदि आप अपने मित्र मॉडल ( self.table_name = "custom_friends_table_name") में एक कस्टम टेबल नाम का उपयोग कर रहे हैं , तो उपयोग करें Person.includes(:friends).where(:custom_friends_table_name => {:id => nil})
जेक

5
रेल 6.1 में @smathy एक अच्छा अद्यतन कहते हैं missingकि वास्तव में क्या करने के लिए विधि इस !
टिम पार्क

172

smathy के पास एक अच्छा रेल 3 उत्तर है।

रेल 5 के लिए , आप left_outer_joinsएसोसिएशन को लोड करने से बचने के लिए उपयोग कर सकते हैं ।

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

की जाँच करें एपीआई डॉक्स । इसे पुल अनुरोध # 12071 में पेश किया गया था ।


क्या इसमें कोई कमी है? मैंने जाँच की और उसने 0.1 मिसे को तेजी से लोड किया। फिर बाहर निकाल दिया
क्वर्टी

यदि आप वास्तव में इसे बाद में एक्सेस करते हैं, तो एसोसिएशन को लोड नहीं करना एक नकारात्मक पक्ष है, लेकिन यदि आप इसे एक्सेस नहीं करते हैं तो एक लाभ है। मेरी साइटों के लिए, एक 0.1ms हिट बहुत नगण्य है, इसलिए .includesलोड समय में अतिरिक्त लागत कुछ ऐसा नहीं होगा जो मैं अनुकूलन के बारे में बहुत अधिक चिंता करूंगा। आपका उपयोग मामला भिन्न हो सकता है।
अनसन

1
और अगर आपके पास अभी तक रेल 5 नहीं है, तो आप Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')यह कर सकते हैं: यह एक गुंजाइश के रूप में भी ठीक काम करता है। मैं अपनी रेल परियोजनाओं में हर समय ऐसा करता हूं।
फ्रैंक

3
इस विधि का बड़ा फायदा मेमोरी सेविंग है। जब आप includesए करते हैं , तो उन सभी एआर ऑब्जेक्ट्स को मेमोरी में लोड किया जाता है, जो एक बुरी चीज हो सकती है क्योंकि टेबल बड़े और बड़े हो जाते हैं। यदि आपको संपर्क रिकॉर्ड तक पहुंच की आवश्यकता नहीं है, left_outer_joinsतो संपर्क को मेमोरी में लोड नहीं करता है। एसक्यूएल अनुरोध की गति समान है, लेकिन समग्र एप्लिकेशन लाभ बहुत बड़ा है।
क्रिस्मसडसन

2
यह सचमुच अच्छा है! धन्यवाद! अब अगर रेल देवता शायद इसे एक सरल के रूप में लागू कर सकते हैं Person.where(contacts: nil)या Person.with(contact: contact)यदि अतिक्रमण का उपयोग करते हुए 'उचितता' में बहुत दूर हैं - लेकिन उस संपर्क को देखते हुए: पहले से ही पार्स किया जा रहा है और एक संघ के रूप में पहचाना जा रहा है, तो यह तर्कसंगत लगता है कि आसानी से काम कर सकते हैं। ...
जस्टिन मैक्सवेल

14

ऐसे व्यक्ति जिनका कोई मित्र नहीं है

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

या कि कम से कम एक दोस्त है

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

आप इस पर Arel के साथ स्कोप स्थापित करके कर सकते हैं 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

और फिर, जिनके पास कम से कम एक दोस्त है:

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

मित्रहीन:

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

2
मुझे लगता है कि आप भी कर सकते हैं: Person.includes (: friends) .where (मित्र: {person: nil})
ReggieB

1
नोट: मर्ज की रणनीति कभी-कभी चेतावनी दे सकती है जैसेDEPRECATION 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

Dmarkow और Unixmonkey के उत्तर दोनों से मुझे वही मिलता है जो मुझे चाहिए - थैंक यू!

मैंने अपने वास्तविक ऐप में दोनों को आज़माया और उनके लिए टाइमिंग प्राप्त की - यहाँ दो स्कोप हैं:

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

इसे एक वास्तविक ऐप के साथ चलाएं - ~ ​​700 'व्यक्ति' रिकॉर्ड के साथ छोटी तालिका - 5 रन का औसत

Unixmonkey का दृष्टिकोण ( :without_friends_v1) 813ms / क्वेरी

dmarkow का दृष्टिकोण ( :without_friends_v2) 891ms / क्वेरी (~ 10% धीमा)

लेकिन फिर यह मेरे साथ हुआ कि मुझे कॉल करने की आवश्यकता नहीं है कि मैं NO के साथ रिकॉर्ड की DISTINCT()...तलाश कर रहा हूं - इसलिए उन्हें केवल संपर्क की सूची होना चाहिए । इसलिए मैंने इस गुंजाइश की कोशिश की:PersonContactsNOT INperson_ids

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

वही परिणाम मिलता है लेकिन औसतन 425 एमएस / कॉल के साथ - लगभग आधा समय ...

अब आपको DISTINCTअन्य समान प्रश्नों की आवश्यकता हो सकती है - लेकिन मेरे मामले के लिए यह ठीक काम करता है।

आपकी सहायता के लिए धन्यवाद


5

दुर्भाग्य से, आप शायद SQL से जुड़े एक समाधान को देख रहे हैं, लेकिन आप इसे एक दायरे में सेट कर सकते हैं और फिर उस दायरे का उपयोग कर सकते हैं:

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

फिर उन्हें प्राप्त करने के लिए, आप बस कर सकते हैं Person.without_friends, और आप इसे अन्य Arel विधियों के साथ भी चेन कर सकते हैं:Person.without_friends.order("name").limit(10)


1

विशेष रूप से पंक्तिबद्ध उपसमुच्चय का संबंध उपवास से नहीं होना चाहिए, विशेष रूप से जैसे कि अभिभावकों के रिकॉर्ड में बच्चे की पंक्ति संख्या और अनुपात बढ़ता है।

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

1

उदाहरण के लिए, एक मित्र द्वारा फ़िल्टर करना:

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

3
यह एक उपशमन के बजाय 2 प्रश्नों का परिणाम देगा।
grepsedawk
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.