Как написать цепочку UNION с ActiveRelation? - PullRequest
14 голосов
/ 08 ноября 2011

Мне нужно иметь возможность связать произвольное количество подвыборов с UNION, используя ActiveRelation.

Меня немного смущает реализация этого ARel, поскольку кажется, что UNION - это двоичная операция.

Тем не менее:

( select_statement_a ) UNION ( select_statement_b ) UNION ( select_statement_c )

является допустимым SQL. Возможно ли это без использования мерзкой подстановки строк?

Ответы [ 4 ]

11 голосов
/ 21 марта 2012

Вы можете сделать немного лучше, чем предлагал Адам Лассек, хотя он на правильном пути. Я только что решил похожую проблему, пытаясь получить список друзей из модели социальной сети. Друзья могут быть получены автоматически различными способами, но я хотел бы иметь дружественный метод запросов 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

А вот примерное решение относительно точного вопроса ОП

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
4 голосов
/ 09 ноября 2011

Из-за способа, которым посетитель ARel генерировал союзы, я продолжал получать ошибки SQL при использовании Arel::Nodes::Union.Похоже, что старомодная интерполяция строк была единственным способом заставить это работать.

У меня есть модель Shift , и я хочу получить коллекцию сдвигов для данного диапазона дат, ограниченнуюдо пяти смен в день.Это метод класса в модели 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)

Необходимость прибегать к интерполяции строк, но, по крайней мере, пользовательские параметры корректируются правильно.Я, конечно, был бы признателен за предложения сделать это чище.

3 голосов
/ 24 ноября 2015

Мне нравится 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) ) )
1 голос
/ 08 февраля 2013

Есть способ заставить эту работу использовать 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))))
...