如何使用ActiveRelation编写UNION链?

我需要能够使用ActiveRelation使用UNION链接任意数量的子选择。

我对ARel的实现有点困惑,因为它似乎假设UNION是一个二进制操作。

然而:

 ( select_statement_a ) UNION ( select_statement_b ) UNION ( select_statement_c ) 

是有效的SQL。 如果不做令人讨厌的字符串替换,这可能吗?

尽管他在正确的轨道上,你可以做得比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 

由于ARel访问者生成联合的方式,我在使用Arel::Nodes::Union时不断出现SQL错误。 看起来像老式的字符串插值是实现这一点的唯一方法。

我有一个Shift模型,我希望得到一个给定日期范围的class次集合,每天限制为五class。 这是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 

(除了ActiveRecord,我使用的是Squeel )

不得不诉诸字符串插值很烦人,但至少用户提供的参数正在被正确消毒。 我当然会赞赏使这个更清洁的建议。

我喜欢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) ) ) 

有一种方法可以使用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))))