2011-11-08 36 views
14

我需要能夠使用ActiveRelation鏈接任意數量的子選擇UNION如何使用ActiveRelation編寫UNION鏈?

我對ARel的實現有點困惑,因爲它似乎假設UNION是一個二進制操作。

但是:

(select_statement_a) UNION (select_statement_b) UNION (select_statement_c) 

是有效的SQL。這可能沒有做不好的字符串替換?

回答

11

雖然他在正確的軌道上,但你可以做得比Adam Lassek提出的要好一些。我剛剛解決了一個類似的問題,試圖從社交網絡模型中獲得朋友列表。朋友可以通過各種方式自動獲取,但我希望有一個ActiveRelation友好的查詢方法,可以處理進一步的鏈接。所以我有

class User 
    has_many :events_as_owner, :class_name => "Event", :inverse_of => :owner, :foreign_key => :owner_id, :dependent => :destroy 
    has_many :events_as_guest, :through => :invitations, :source => :event 

     def friends 


     friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}} 
     friends_as_hosts = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}} 

     User.where do 
      (id.in friends_as_guests.select{id} 
     ) | 
      (id.in friends_as_hosts.select{id} 
     ) 
     end 
     end 

end 

它利用Squeels子查詢支持。生成的SQL是

SELECT "users".* 
FROM "users" 
WHERE (("users"."id" IN (SELECT "users"."id" 
          FROM "users" 
            INNER JOIN "invitations" 
            ON "invitations"."user_id" = "users"."id" 
            INNER JOIN "events" 
            ON "events"."id" = "invitations"."event_id" 
          WHERE "events"."owner_id" = 87) 
      OR "users"."id" IN (SELECT "users"."id" 
           FROM "users" 
             INNER JOIN "events" 
             ON "events"."owner_id" = "users"."id" 
             INNER JOIN "invitations" 
             ON "invitations"."user_id" = 
              "users"."id" 
           WHERE "invitations"."user_id" = 87))) 

在您需要的組件數量可變的證明有輕微的修改上面的代碼

def friends 


    friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}} 
    friends_as_hosts = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}} 

    components = [friends_as_guests, friends_as_hosts] 

    User.where do 
     components = components.map { |c| id.in c.select{id} } 
     components.inject do |s, i| 
     s | i 
     end 
    end 


    end 

這裏是一個粗略的猜測,對解決方案的另一種模式OP的具體問題

class Shift < ActiveRecord::Base 
    def self.limit_per_day(options = {}) 
    options[:start] ||= Date.today 
    options[:stop] ||= Date.today.next_month 
    options[:per_day] ||= 5 

    queries = (options[:start]..options[:stop]).map do |day| 

     where{|s| s.scheduled_start >= day}. 
     where{|s| s.scheduled_start < day.tomorrow}. 
     limit(options[:per_day]) 

    end 

    where do 
     queries.map { |c| id.in c.select{id} }.inject do |s, i| 
     s | i 
     end 
    end 
    end 
end 
+0

但是,這是一個稍微不同的情況。我需要能夠鏈接任意數量的選擇。所以我仍然不得不求助於字符串插值,而不管我是使用'UNION'還是'OR'。除非你可以用Squeel/ARel建議一種方法來實現這一點? –

+0

這可以做到。它只需要一點點的魔力。讓我想想。可能可以使用Enumerable注入來完成。如果它有效,我會給它一個更新的答案。 – bradgonesurfing

+0

好的。我做了一個等同的功能,首先將組件放入數組中,然後組合這些組件。請記住,SQueel僅僅是一些缺乏魔法的方法而已。您可以在where塊中執行普通ruby,並動態構建查詢。 – bradgonesurfing

4

由於ARel訪問者生成工會的方式,在使用Arel::Nodes::Union時,我不斷收到SQL錯誤。看起來老式的字符串插值是實現這種工作的唯一方法。

我有一個Shift模型,我想獲得給定日期範圍內的班次集合,每天限制爲五班倒。這是對變速模型類方法:

def limit_per_day(options = {}) 
    options[:start] ||= Date.today 
    options[:stop] ||= Date.today.next_month 
    options[:per_day] ||= 5 

    queries = (options[:start]..options[:stop]).map do |day| 

    select{id}. 
    where{|s| s.scheduled_start >= day}. 
    where{|s| s.scheduled_start < day.tomorrow}. 
    limit(options[:per_day]) 

    end.map{|q| "(#{ q.to_sql })" } 

    where %{"shifts"."id" in (#{queries.join(' UNION ')})} 
end 

(我用Squeel除了ActiveRecord的)

不必訴諸字符串插值很討厭,但至少用戶提供的參數正確消毒。我當然會很感激提出這個更清潔的建議。

1

有一種方法使用AREL,使這項工作:

tc=TestColumn.arel_table 
return TestColumn.where(tc[:id] 
      .in(TestColumn.select(:id) 
         .where(:attr1=>true) 
         .union(TestColumn.select(:id) 
              .select(:id) 
              .where(:attr2=>true)))) 
+1

我不認爲你瞭解這個問題。這是兩個查詢之間的單個UNION,工作正常。我需要能夠將UNION鏈接到任意長度。 UNION語義在SQL實現中可能不同 - 在這種情況下,我使用的是PostgreSQL。 –

+0

我做過了,這是最接近我使用arel來執行聯合的。 –

+0

無論如何,你可以通過執行'Domain.where(attr => value).union(Domain.where(attr2 => value))'''也許你可以在'to_sql'方法之後得到一個Arel :: Node,並將它傳遞給'find_by_sql' –

3

我喜歡Squeel。但不要使用它。所以我來到這個解決方案(Arel 4.0.2)

def build_union(left, right) 
    if right.length > 1 
    Arel::Nodes::UnionAll.new(left, build_union(right[0], right[1..-1])) 
    else 
    Arel::Nodes::UnionAll.new(left, right[0]) 
    end 
end 

managers = [select_manager_1, select_manager_2, select_manager_3] 
build_union(managers[0], managers[1..-1]).to_sql 
# => ((SELECT table1.* from table1) 
# UNION ALL 
# ((SELECT table2.* from table2) 
# UNION ALL 
# (SELECT table3.* from table3))) 
+0

看來他們爲非二元工會添加了'Arel :: Nodes :: UnionAll'。當我可以使用Arel 4時,我會再次訪問。 –

+0

非常感謝!我正在尋找這個。 –