Как написать цепочку UNION с ActiveRelation?

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

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

Однако:

( select_statement_a ) UNION ( select_statement_b ) UNION ( select_statement_c )

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


person Adam Lassek    schedule 08.11.2011    source источник


Ответы (4)


Вы можете сделать немного лучше, чем предложил Адам Лассек, хотя он на правильном пути. Я только что решил аналогичную проблему, пытаясь получить список друзей из модели социальной сети. Друзей можно приобретать автоматически различными способами, но я хотел бы иметь дружественный к 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
person bradgonesurfing    schedule 21.03.2012
comment
Однако это немного другой случай. Мне нужно иметь возможность связать произвольное количество выборок вместе. Поэтому мне все равно придется прибегать к интерполяции строк независимо от того, использую ли я UNION или OR. Если вы не можете предложить способ сделать это с помощью Squeel/ARel? - person Adam Lassek; 21.03.2012
comment
Это можно сделать. Это просто требует немного магии скрипа. Дай мне подумать об этом. Вероятно, это можно сделать с помощью Enumerable inject. Я попробую и обновлю ответ, если он сработает. - person bradgonesurfing; 21.03.2012
comment
Ok. Я сделал эквивалентную функцию, которая сначала помещает компоненты в массив, а затем объединяет эти компоненты. Просто помните, что SQueel — это просто рубин, в котором отсутствует какая-то магия метода. Вы можете делать обычный ruby ​​внутри блоков where и динамически строить запросы. - person bradgonesurfing; 21.03.2012
comment
queries.map{...}.reduce(:|) тоже подойдет и выглядит как смайлик. - person Petr ''Bubák'' Šedivý; 23.07.2014
comment
Сгенерированная @bradgonesurfing часть 1 sql сломается для оракула, если внутренний запрос вернет более 1000 строк - person ant; 03.09.2015

Из-за того, как посетитель 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)

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

person Adam Lassek    schedule 09.11.2011

Мне нравится Сквел. Но не используйте его. Итак, я пришел к этому решению (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) ) )
person NilColor    schedule 24.11.2015
comment
Кажется, они добавили Arel::Nodes::UnionAll для небинарных союзов. Я вернусь к этому, когда смогу использовать Arel 4. - person Adam Lassek; 25.11.2015
comment
Большое спасибо! Я искал это. - person Ganesh Mohan; 27.09.2016
comment
у меня не работает я получаю Cannot visit ModelName::ActiveRecord_Relation - person Pants; 03.11.2020

Есть способ сделать эту работу с помощью 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))))
person Pedro Rolo    schedule 08.02.2013
comment
Я не думаю, что вы поняли вопрос. Это один UNION между двумя запросами, который отлично работает. Мне нужно иметь возможность объединять СОЮЗЫ вместе до произвольной длины. Возможно, семантика UNION различается в разных реализациях SQL — в этом случае я использую PostgreSQL. - person Adam Lassek; 15.02.2013
comment
Я сделал, это самое близкое, что я подошел к выполнению союза с использованием arel. - person Pedro Rolo; 15.02.2013
comment
В любом случае, вы можете получить Arel::Node, выполнив Domain.where(attr=>value).union(Domain.where(attr2=>value)), возможно, вы сможете впоследствии использовать метод to_sql и передать его find_by_sql - person Pedro Rolo; 15.02.2013