ActiveRecord क्वेरी यूनियन


90

मैंने रेल के क्वेरी इंटरफ़ेस पर रूबी के साथ कुछ जटिल प्रश्न (कम से कम मेरे लिए) लिखे हैं:

watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})

ये दोनों ही प्रश्न अपने आप ठीक होते हैं। दोनों पोस्ट ऑब्जेक्ट वापस करते हैं। मैं इन पोस्ट्स को एक सिंगल एक्टिवेशन में जोड़ना चाहूंगा। चूंकि कुछ बिंदु पर सैकड़ों हजारों पद हो सकते हैं, इसलिए इसे डेटाबेस स्तर पर करने की आवश्यकता है। यदि यह एक MySQL क्वेरी होती, तो मैं बस UNIONऑपरेटर को उपयोगकर्ता कर सकता था । क्या किसी को पता है कि क्या मैं RoR के क्वेरी इंटरफेस के साथ कुछ ऐसा कर सकता हूं?


आपको गुंजाइश का उपयोग करने में सक्षम होना चाहिए । 2 स्कोप बनाएं और फिर उन दोनों को कॉल करें Post.watched_news_posts.watched_topic_posts। आप जैसी चीजों के लिए स्कोप के पैरामीटर में भेजने के लिए आवश्यकता हो सकती है :user_idऔर :topic
ज़ब्बा

6
सलाह के लिये धन्यवाद। डॉक्स के अनुसार, "एक गुंजाइश एक डेटाबेस क्वेरी के संकुचन का प्रतिनिधित्व करता है"। मेरे मामले में, मैं उन पोस्टों की तलाश नहीं कर रहा हूँ जो दोनों watch_news_posts और watched_topic_posts में हैं। बल्कि, मैं उन पोस्टों की तलाश कर रहा हूं जो watched_news_posts या watched_topic_posts में हैं, जिनमें कोई डुप्लिकेट अनुमत नहीं है। क्या यह अभी भी scopes के साथ पूरा करना संभव है?
LandonSchropp

1
वास्तव में संभव नहीं है। Github पर संघ नामक एक प्लगइन है, लेकिन यह पुराने-स्कूल वाक्यविन्यास (क्लास पद्धति और हैश-शैली क्वेरी params) का उपयोग करता है, अगर यह आपके साथ अच्छा है तो मैं कहूंगा कि मैं इसके साथ जाऊंगा ... अन्यथा इसे लंबे तरीके से लिखें अपने दायरे में find_by_sql।
jenjenut233

1
मैं jenjenut233 से सहमत हूं, और मुझे लगता है कि आप कुछ ऐसा कर सकते हैं find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")। मैंने इसका परीक्षण नहीं किया है, इसलिए मुझे बताएं कि यदि आप इसे आजमाते हैं तो यह कैसा होगा। इसके अलावा, वहाँ शायद कुछ ARel कार्यक्षमता है कि काम करेगा।
ओगज़ जूल

2
खैर मैं एसक्यूएल प्रश्नों के रूप में प्रश्नों को फिर से लिखता हूं। वे अब काम करते हैं, लेकिन दुर्भाग्य find_by_sqlसे अन्य श्रृंखलाबद्ध प्रश्नों के साथ उपयोग नहीं किया जा सकता है, जिसका अर्थ है कि मुझे अब मेरे will_paginate फ़िल्टर और प्रश्नों को भी फिर से लिखना होगा। ActiveRecord किसी unionऑपरेशन का समर्थन क्यों नहीं करता है ?
लैंडनश्रोप जूला

जवाबों:


93

यहाँ एक छोटा सा मॉड्यूल है जो मैंने लिखा है कि आप कई स्कोपों ​​को UNION करने की अनुमति देता है। यह ActiveRecord :: Relation के उदाहरण के रूप में भी परिणाम देता है।

module ActiveRecord::UnionScope
  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    def union_scope(*scopes)
      id_column = "#{table_name}.id"
      sub_query = scopes.map { |s| s.select(id_column).to_sql }.join(" UNION ")
      where "#{id_column} IN (#{sub_query})"
    end
  end
end

यहां बताइए गिस्ट: https://gist.github.com/tlowrimore/5162327

संपादित करें:

जैसा कि अनुरोध किया गया है, यहां एक उदाहरण है कि यूनियनस्कोप कैसे काम करता है:

class Property < ActiveRecord::Base
  include ActiveRecord::UnionScope

  # some silly, contrived scopes
  scope :active_nearby,     -> { where(active: true).where('distance <= 25') }
  scope :inactive_distant,  -> { where(active: false).where('distance >= 200') }

  # A union of the aforementioned scopes
  scope :active_near_and_inactive_distant, -> { union_scope(active_nearby, inactive_distant) }
end

2
यह वास्तव में ऊपर सूचीबद्ध अन्य अन्य लोगों के लिए एक और अधिक पूर्ण जवाब है। बहुत अच्छा काम करता है!
घीस

उपयोग का उदाहरण अच्छा होगा।
ciembor

अनुरोध के अनुसार, मैंने एक उदाहरण जोड़ा है।
टिम लोइमोर

3
समाधान "लगभग" सही है और मैंने इसे +1 दिया, लेकिन मैं एक समस्या में भाग गया, जिसे मैंने यहां तय किया: gist.github.com/lsiden/260167a4d3574a580d97
लॉरेंस I. सिडेन

7
त्वरित चेतावनी: यह विधि MySQL के साथ एक प्रदर्शन के नजरिए से अत्यधिक समस्याग्रस्त है, क्योंकि उपश्रम को तालिका में प्रत्येक रिकॉर्ड के लिए निर्भर और निष्पादित किया जाएगा (देखें percona.com/blog/2010/10/25/mysql-limit-part -3-उपश्रेणियाँ )।
शोस्ती

71

मुझे इस समस्या का भी सामना करना पड़ा है, और अब मेरी जाने की रणनीति एसक्यूएल (हाथ से या to_sqlमौजूदा गुंजाइश पर) का उत्पादन करना है और फिर इसे fromक्लॉज में चिपका देना है। मैं यह स्वीकार नहीं कर सकता कि यह आपके स्वीकृत तरीके से अधिक कुशल है, लेकिन यह आंखों पर अपेक्षाकृत आसान है और आपको एक सामान्य एआरएल ऑब्जेक्ट वापस देता है।

watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})

Post.from("(#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}) AS posts")

आप इसे दो अलग-अलग मॉडलों के साथ भी कर सकते हैं, लेकिन आपको यह सुनिश्चित करने की आवश्यकता है कि वे दोनों "समान दिखें" जो कि UNION के अंदर हैं - आप selectदोनों प्रश्नों का उपयोग यह सुनिश्चित करने के लिए कर सकते हैं कि वे एक ही कॉलम का उत्पादन करेंगे।

topics = Topic.select('user_id AS author_id, description AS body, created_at')
comments = Comment.select('author_id, body, created_at')

Comment.from("(#{comments.to_sql} UNION #{topics.to_sql}) AS comments")

मान लें कि अगर हमारे पास दो अलग-अलग मॉडल हैं तो कृपया मुझे बताएं कि संयुक्त राष्ट्र के लिए क्वेरी क्या होगी।
चित्रा

बहुत मददगार जवाब। भविष्य के पाठकों के लिए, अंतिम "AS कमेंट्स" भाग को याद रखें क्योंकि एक्टिवरकॉर्ड क्वेरी को 'Select' कमेंट्स '' के रूप में बनाता है। "*" FROM "... यदि आप सेट किए गए सेट का नाम निर्दिष्ट नहीं करते हैं या एक अलग नाम निर्दिष्ट करते हैं जैसे "एएस फू", अंतिम एसक्यूएल निष्पादन विफल हो जाएगा।
हेज़िको 15

1
यह ठीक वही है जिसकी मुझे तलाश थी। मैंने ActiveRecord का विस्तार किया :: #orमेरे रेल 4 परियोजना में समर्थन के लिए संबंध । उसी मॉडल को मानते हुए:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
एम। व्याट

11

ओलिव्स के जवाब के आधार पर, मैं इस समस्या का एक और समाधान लेकर आया। यह एक हैक की तरह थोड़ा सा महसूस करता है, लेकिन यह एक उदाहरण देता है ActiveRelation, जो कि मैं पहले स्थान पर था।

Post.where('posts.id IN 
      (
        SELECT post_topic_relationships.post_id FROM post_topic_relationships
          INNER JOIN "watched" ON "watched"."watched_item_id" = "post_topic_relationships"."topic_id" AND "watched"."watched_item_type" = "Topic" WHERE "watched"."user_id" = ?
      )
      OR posts.id IN
      (
        SELECT "posts"."id" FROM "posts" INNER JOIN "news" ON "news"."id" = "posts"."news_id" 
        INNER JOIN "watched" ON "watched"."watched_item_id" = "news"."id" AND "watched"."watched_item_type" = "News" WHERE "watched"."user_id" = ?
      )', id, id)

अगर किसी के पास इसे अनुकूलित करने या प्रदर्शन को बेहतर बनाने के लिए कोई सुझाव है, तो भी मैं इसकी सराहना करूंगा, क्योंकि यह अनिवार्य रूप से तीन प्रश्नों का निष्पादन कर रहा है और थोड़ा बेमानी लगता है।


मैं इसके साथ एक ही काम कैसे कर सकता था: gist.github.com/2241307 ताकि यह एक Arre class के बजाय AR :: Relation क्लास बनाता है?
मार्क

10

आप ब्रायन हेम्पेल के सक्रिय_रेकोर्ड_यूनियन मणि का भी उपयोग कर सकते हैं जो स्कोप के लिए ActiveRecordएक unionविधि के साथ फैली हुई है ।

आपकी क्वेरी इस तरह होगी:

Post.joins(:news => :watched).
  where(:watched => {:user_id => id}).
  union(Post.joins(:post_topic_relationships => {:topic => :watched}
    .where(:watched => {:user_id => id}))

उम्मीद है कि यह अंततः ActiveRecordकिसी दिन में विलय हो जाएगा।


8

कैसा रहेगा...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end

15
चेतावनी दें कि यह एक के बजाय तीन प्रश्नों का प्रदर्शन करेगा, क्योंकि प्रत्येक pluckकॉल अपने आप में एक प्रश्न है।
याकूब एवलिन

3
यह एक बहुत अच्छा समाधान है, क्योंकि यह किसी सरणी को वापस नहीं करता है, इसलिए तब आप इसका उपयोग कर सकते हैं .orderया .paginateतरीके ... यह orm classes रखता है
mariowise

उपयोगी यदि स्कोप एक ही मॉडल के हैं, लेकिन यह प्लक्स के कारण दो प्रश्न उत्पन्न करेगा।
jmjm

6

क्या आप एक UNION के बजाय OR का उपयोग कर सकते हैं?

तब आप कुछ ऐसा कर सकते थे:

Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)

(चूंकि आप दो बार देखे गए तालिका में शामिल होते हैं, मुझे यह भी निश्चित नहीं है कि क्वेरी के लिए तालिकाओं के नाम क्या होंगे)

चूंकि बहुत सारे जोड़ हैं, इसलिए यह डेटाबेस पर काफी भारी हो सकता है, लेकिन यह अनुकूलित करने में सक्षम हो सकता है।


2
आपके इतनी देर से वापस आने के लिए क्षमा करें, लेकिन मैं पिछले कुछ दिनों से छुट्टी पर हूं। जब मैंने आपके उत्तर की कोशिश की तो समस्या यह थी कि जुड़ने की विधि से दोनों तालिकाओं को जोड़ा जा रहा था, बजाय इसके कि दो अलग-अलग प्रश्नों की तुलना की जा सके। हालाँकि, आपका विचार अच्छा था और मुझे एक और विचार दिया। सहायता के लिए धन्यवाद।
LandonSchropp

यूएनआईओएन की तुलना में ओआर का उपयोग करना धीमा है, इसके बजाय यूनिअन के लिए किसी भी समाधान की तलाश करना
निचे

5

यकीनन, यह पठनीयता में सुधार करता है, लेकिन जरूरी नहीं कि प्रदर्शन:

def my_posts
  Post.where <<-SQL, self.id, self.id
    posts.id IN 
    (SELECT post_topic_relationships.post_id FROM post_topic_relationships
    INNER JOIN watched ON watched.watched_item_id = post_topic_relationships.topic_id 
    AND watched.watched_item_type = "Topic" 
    AND watched.user_id = ?
    UNION
    SELECT posts.id FROM posts 
    INNER JOIN news ON news.id = posts.news_id 
    INNER JOIN watched ON watched.watched_item_id = news.id 
    AND watched.watched_item_type = "News" 
    AND watched.user_id = ?)
  SQL
end

यह विधि एक ActiveRecord :: Relation देता है, इसलिए आप इसे इस तरह कह सकते हैं:

my_posts.order("watched_item_type, post.id DESC")

आप कहाँ से पोस्टसीड कर रहे हैं?
बेर्टन77

दो स्व.याद पैरामीटर हैं क्योंकि स्व.फिड को SQL में दो बार संदर्भित किया जाता है - दो प्रश्न चिह्न देखें।
रिचर्ड्सन

यह एक उपयोगी उदाहरण है कि कैसे एक UNION क्वेरी करें और एक ActiveRecord :: Relation को वापस लें। धन्यवाद।
फिटर मैन

क्या आपके पास इस प्रकार के एसडीएल प्रश्नों को उत्पन्न करने का एक उपकरण है - आपने वर्तनी की गलतियों आदि के बिना यह कैसे किया?
BKSpurgeon

2

एक सक्रिय_क्रिक_नियन रत्न है। सहायक हो सकता है

https://github.com/brianhempel/active_record_union

ActiveRecordUnion के साथ, हम कर सकते हैं:

वर्तमान उपयोगकर्ता (ड्राफ्ट) पोस्ट और सभी प्रकाशित पोस्ट जो किसी भी current_user.posts.union(Post.published) निम्न SQL के बराबर है:

SELECT "posts".* FROM (
  SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" = 1
  UNION
  SELECT "posts".* FROM "posts"  WHERE (published_at < '2014-07-19 16:04:21.918366')
) posts

1

मुझे आपके द्वारा आवश्यक दो प्रश्नों को चलाने और वापस आने वाले अभिलेखों के संयोजन को जोड़ना होगा:

@posts = watched_news_posts + watched_topics_posts

या, कम से कम इसका परीक्षण करें। क्या आपको लगता है कि माणिक में सरणी संयोजन बहुत धीमी गति से होगा? समस्या के आस-पास होने के लिए सुझाए गए प्रश्नों को देखते हुए, मुझे विश्वास नहीं हो रहा है कि प्रदर्शन के अंतर में यह महत्वपूर्ण होगा।


वास्तव में @ पोस्ट करना = देखे गए_न्यूज़_पोस्ट्स और देखे गए_टॉपिक्स_पोस्ट्स बेहतर हो सकते हैं क्योंकि यह एक चौराहा है और ठगी से बच जाएगा।
जेफरी एलन ली

1
मैं इस धारणा के अधीन था कि ActiveRelation ने अपने रिकॉर्ड को आलसी तरीके से लोड किया है। यदि आप रूबी में सरणियों को काटते हैं तो क्या आप नहीं खोएंगे?
LandonSchropp

जाहिरा तौर पर एक संघ जो एक संबंध लौटाता है रेल में देव के अधीन है, लेकिन मुझे नहीं पता कि यह किस संस्करण में होगा
जेफरी एलन ली

1
इसके बदले यह सरणी, इसके दो अलग-अलग क्वेरी परिणाम मर्ज करते हैं।
एलेक्सजग

1

इसी तरह के एक मामले में मैंने दो सरणियों का सारांश दिया और उपयोग किया Kaminari:paginate_array()। बहुत अच्छा और काम कर समाधान। मैं उपयोग करने में असमर्थ था where(), क्योंकि मुझे order()एक ही टेबल पर अलग-अलग दो परिणामों की राशि चाहिए ।


1

कम समस्याओं और आसान का पालन करें:

    def union_scope(*scopes)
      scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) }
    end

तो अंत में:

union_scope(watched_news_posts, watched_topic_posts)

1
मैंने इसे थोड़ा बदल दिया: scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }Thx!
13

0

इलियट नेल्सन ने अच्छा जवाब दिया, उस मामले को छोड़कर जहां कुछ संबंध खाली हैं। मैं ऐसा कुछ करूंगा:

def union_2_relations(relation1,relation2)
sql = ""
if relation1.any? && relation2.any?
  sql = "(#{relation1.to_sql}) UNION (#{relation2.to_sql}) as #{relation1.klass.table_name}"
elsif relation1.any?
  sql = relation1.to_sql
elsif relation2.any?
  sql = relation2.to_sql
end
relation1.klass.from(sql)

समाप्त


0

Heres कैसे मैं अपने स्वयं के रूबी पर रेल अनुप्रयोग का उपयोग कर SQL प्रश्नों में शामिल हो गया।

आप अपने स्वयं के कोड पर प्रेरणा के रूप में नीचे का उपयोग कर सकते हैं।

class Preference < ApplicationRecord
  scope :for, ->(object) { where(preferenceable: object) }
end

नीचे UNION है जहाँ मैं एक साथ scopes में शामिल हुआ।

  def zone_preferences
    zone = Zone.find params[:zone_id]
    zone_sql = Preference.for(zone).to_sql
    region_sql = Preference.for(zone.region).to_sql
    operator_sql = Preference.for(Operator.current).to_sql

    Preference.from("(#{zone_sql} UNION #{region_sql} UNION #{operator_sql}) AS preferences")
  end
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.