2011-11-08 12 views
14

Devo essere in grado di concatenare un numero arbitrario di sottoselezioni con UNION usando ActiveRelation.Come scrivere una catena UNION con ActiveRelation?

Sono un po 'confuso dall'implementazione di ARel di questo, poiché sembra presupporre che UNION è un'operazione binaria.

Tuttavia:

(select_statement_a) UNION (select_statement_b) UNION (select_statement_c) 

è SQL valida. Questo è possibile senza fare brutte sostituzioni con le stringhe?

risposta

11

Si può fare un po 'meglio di quello che Adam Lassek ha proposto anche se è sulla strada giusta. Ho appena risolto un problema simile cercando di ottenere un elenco di amici da un modello di social network. Gli amici possono essere acquisiti automaticamente in vari modi, ma mi piacerebbe avere un metodo di query amichevole ActiveRelation in grado di gestire ulteriori concatenazioni. Così ho

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 

che sfrutta il supporto di subquery di Squeels. SQL generato è

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))) 

Un modello alternativo in cui avete bisogno di un numero variabile di componenti è dimostrata con una leggera modifica al codice di cui sopra

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 

Ed ecco un occhio e croce per quanto riguarda la soluzione per il domanda esatta di 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

Questo è un caso leggermente diverso, tuttavia. Devo essere in grado di concatenare un numero arbitrario di selezioni insieme. Quindi dovrò ancora ricorrere all'interpolazione delle stringhe indipendentemente dal fatto che io usi 'UNION' o' OR'. A meno che tu non possa suggerire un modo per farlo con Squeel/ARel? –

+0

Questo può essere fatto. Richiede solo un po 'di magia da squeel. Fammici pensare. Probabilmente si potrebbe fare con Enumerable inject. Ci provo e aggiorno la risposta se funziona. – bradgonesurfing

+0

Ok. Ho creato una funzione equivalente che mette i componenti in una matrice prima e poi combina quei componenti.Ricorda solo che SQueel è semplicemente un rubino con qualche metodo che manca di magia. Puoi fare il ruby ​​normale all'interno dei blocchi dove e creare query in modo dinamico. – bradgonesurfing

4

A causa del modo in cui il visitatore di ARel stava generando i sindacati, ho riscontrato errori SQL durante l'utilizzo di Arel::Nodes::Union. Sembra che l'interpolazione delle corde vecchio stile fosse l'unico modo per farlo funzionare.

Ho un modello Shift e desidero ottenere una raccolta di turni per un determinato intervallo di date, limitato a cinque turni al giorno. Si tratta di un metodo di classe sul modello Spostamento:

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 

(sto usando Squeel oltre ad ActiveRecord)

dover ricorrere a stringa-interpolazione è fastidioso, ma almeno i parametri forniti dall'utente sono essere igienizzato correttamente Ovviamente apprezzerei i suggerimenti per rendere questo più pulito.

1

C'è un modo per fare questo lavoro utilizzando 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

Non credo di aver capito la domanda. Questa è una UNION tra due query, che funziona bene. Devo essere in grado di collegare le UNION a una lunghezza arbitraria. È possibile che la semantica UNION sia diversa tra le implementazioni SQL - in questo caso sto usando PostgreSQL. –

+0

Ho fatto, questo è il più vicino che sono venuto a eseguire un sindacato utilizzando Arel. –

+0

Ad ogni modo, puoi ottenere un Arel :: Node eseguendo 'Domain.where (attr => value) .union (Domain.where (attr2 => value))' forse puoi in seguito il metodo 'to_sql' e passarlo a 'find_by_sql' –

3

Mi piace Squeel. Ma non usarlo. Così sono arrivato a questa soluzione (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

Sembra che abbiano aggiunto' Arel :: Nodes :: UnionAll' per le unioni non binarie. Lo rivisiterò quando posso usare Arel 4. –

+0

Grazie mille! Stavo cercando questo. –