ActiveRecord查询联合


90

我已经使用Ruby on Rail的查询界面编写了一些复杂的查询(至少对我来说):

watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})

这两个查询本身都能正常工作。两者都返回Post对象。我想将这些帖子合并为一个ActiveRelation。由于某个时候可能有成千上万的帖子,因此需要在数据库级别完成。如果是MySQL查询,我可以简单地使用UNION运算符。有人知道我是否可以使用RoR的查询界面执行类似的操作吗?


您应该能够使用scope。创建2个作用域,然后像一样调用它们Post.watched_news_posts.watched_topic_posts。您可能需要将参数发送到范围,例如:user_id:topic
2011年

6
谢谢你的建议。根据文档,“作用域表示数据库查询范围的缩小”。就我而言,我不是要查找watched_news_posts和watched_topic_posts中的帖子。相反,我正在寻找watched_news_posts或watched_topic_posts中的帖子,不允许重复。合并范围仍然可以实现吗?
LandonSchropp 2011年

1
开箱即用真的不太可能。在github上有一个名为union的插件,但是它使用了老式的语法(类方法和哈希样式的查询参数),如果这对您来说很酷,我会说去吧……否则就将它写得很长您范围内的find_by_sql。
jenjenut233 2011年

1
我同意jenjenut233,我认为您可以做类似的事情find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")。我还没有测试过,所以如果您尝试一下,请告诉我。另外,可能还有一些ARel功能会起作用。
奥格兹向导

2
好吧,我将查询重写为SQL查询。它们现在find_by_sql可以工作,但是不幸的是不能与其他可链接查询一起使用,这意味着我现在也必须重写will_paginate过滤器和查询。为什么ActiveRecord不支持union操作?
LandonSchropp 2011年

Answers:


93

这是我编写的一个快速小模块,它允许您UNION多个范围。它还将结果作为ActiveRecord :: Relation的实例返回。

module ActiveRecord::UnionScope
  def self.included(base)
    base.send :extend, ClassMethods
  end

  module ClassMethods
    def union_scope(*scopes)
      id_column = "#{table_name}.id"
      sub_query = scopes.map { |s| s.select(id_column).to_sql }.join(" UNION ")
      where "#{id_column} IN (#{sub_query})"
    end
  end
end

这是要点:https : //gist.github.com/tlowrimore/5162327

编辑:

根据要求,以下是UnionScope的工作方式示例:

class Property < ActiveRecord::Base
  include ActiveRecord::UnionScope

  # some silly, contrived scopes
  scope :active_nearby,     -> { where(active: true).where('distance <= 25') }
  scope :inactive_distant,  -> { where(active: false).where('distance >= 200') }

  # A union of the aforementioned scopes
  scope :active_near_and_inactive_distant, -> { union_scope(active_nearby, inactive_distant) }
end

2
这确实是一种更完整的答案,可以回答上面列出的其他方法。很棒!
ghayes

使用示例将是不错的。
ciembor 2014年

根据要求,我添加了一个示例。
Tim Lowrimore

3
解决方案“几乎”是正确的,我给了+1,但我遇到了一个在这里解决的问题:gist.github.com/lsiden/260167a4d3574a580d97
Lawrence I. Siden

7
快速警告:从MySQL的性能角度来看,此方法存在很大问题,因为对于表中的每个记录,子查询都将被视为从属查询并执行(请参见percona.com/blog/2010/10/25/mysql-limitations-part -3-子查询)。
shosti 2014年

70

我也遇到了这个问题,现在我的首选策略是生成SQL(手动或to_sql在现有作用域上使用),然后将其粘贴在from子句中。我不能保证它比您接受的方法更有效,但是它在眼睛上相对容易实现,并且可以为您提供普通的ARel对象。

watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id})
watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id})

Post.from("(#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}) AS posts")

您也可以使用两个不同的模型来执行此操作,但是您需要确保它们在UNION中都“看起来相同”-您可以select在两个查询上使用以确保它们将产生相同的列。

topics = Topic.select('user_id AS author_id, description AS body, created_at')
comments = Comment.select('author_id, body, created_at')

Comment.from("(#{comments.to_sql} UNION #{topics.to_sql}) AS comments")

假设我们有两个不同的模型,那么请让我知道对unoin的查询是什么。
Chitra

非常有帮助的答案。对于将来的读者,请记住最后一个“ AS注释”部分,因为activerecord将查询构造为“ SELECT”“注释”。“ *” FROM“ ...如果您未指定联合集的名称或指定其他名称,例如“ AS foo”,最终的SQL执行将失败
HeyZiko

1
这正是我想要的。我扩展了ActiveRecord :: Relation以#or在我的Rails 4项目中提供支持。假设使用相同的模型:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
M. Wyatt

11

根据Olives的回答,我确实提出了解决该问题的另一种方法。感觉有点像黑客入侵,但它返回的实例ActiveRelation,这是我最初追求的。

Post.where('posts.id IN 
      (
        SELECT post_topic_relationships.post_id FROM post_topic_relationships
          INNER JOIN "watched" ON "watched"."watched_item_id" = "post_topic_relationships"."topic_id" AND "watched"."watched_item_type" = "Topic" WHERE "watched"."user_id" = ?
      )
      OR posts.id IN
      (
        SELECT "posts"."id" FROM "posts" INNER JOIN "news" ON "news"."id" = "posts"."news_id" 
        INNER JOIN "watched" ON "watched"."watched_item_id" = "news"."id" AND "watched"."watched_item_type" = "News" WHERE "watched"."user_id" = ?
      )', id, id)

如果有人对它进行优化或提高性能有任何建议,我还是会很感激,因为它实际上是在执行三个查询,因此有点多余。


我该如何做同样的事情:gist.github.com/2241307 ,以便它创建AR :: Relation类而不是Array类?
马克2012年

10

您还可以使用Brian Hempelactive_record_union宝石,该宝石ActiveRecordunion范围方法扩展。

您的查询将如下所示:

Post.joins(:news => :watched).
  where(:watched => {:user_id => id}).
  union(Post.joins(:post_topic_relationships => {:topic => :watched}
    .where(:watched => {:user_id => id}))

希望这将最终合并到ActiveRecord某一天。


8

怎么样...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end

15
请注意,这将执行三个查询而不是一个查询,因为每个pluck调用本身就是一个查询。
JacobEvelyn 2014年

3
这是一个非常好的解决方案,因为它不返回数组,所以您可以使用.order.paginate方法...它保留了orm类
mariowise 2015年

如果范围是同一模型,则很有用,但是由于选择,这将生成两个查询。
jmjm'3

6

您可以使用OR代替UNION吗?

然后,您可以执行以下操作:

Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)

(由于您两次连接了被监视的表,所以我不太确定查询的表名是什么)

由于存在许多联接,因此数据库上的联接可能也很繁重,但是可能可以对其进行优化。


2
很抱歉这么晚回复您,但最近几天我一直在休假。我尝试回答时遇到的问题是joins方法导致两个表都被连接,而不是两个可以进行比较的独立查询。但是,您的想法很合理,确实给了我另一个想法。谢谢您的帮助。
LandonSchropp 2011年

使用OR进行选择要比UNION慢,想知道是否有UNION的解决方案
Nich 2016年

5

可以说,这可以提高可读性,但不一定可以提高性能:

def my_posts
  Post.where <<-SQL, self.id, self.id
    posts.id IN 
    (SELECT post_topic_relationships.post_id FROM post_topic_relationships
    INNER JOIN watched ON watched.watched_item_id = post_topic_relationships.topic_id 
    AND watched.watched_item_type = "Topic" 
    AND watched.user_id = ?
    UNION
    SELECT posts.id FROM posts 
    INNER JOIN news ON news.id = posts.news_id 
    INNER JOIN watched ON watched.watched_item_id = news.id 
    AND watched.watched_item_type = "News" 
    AND watched.user_id = ?)
  SQL
end

此方法返回ActiveRecord :: Relation,因此您可以这样调用它:

my_posts.order("watched_item_type, post.id DESC")

您从哪里得到posts.id?
berto77

有两个self.id参数,因为self.id在SQL中被两次引用-请参阅两个问号。
richardsun

这是一个有用的示例,说明如何进行UNION查询并返回ActiveRecord :: Relation。谢谢。
Fitter Man 2014年

您是否具有生成此类SDL查询的工具-如何做到没有拼写错误等?
BKSpurgeon

2

有一个active_record_union宝石。可能会有所帮助

https://github.com/brianhempel/active_record_union

使用ActiveRecordUnion,我们可以执行以下操作:

当前用户的(草稿)帖子以及来自任何人的所有已发布的帖子, current_user.posts.union(Post.published) 其等效于以下SQL:

SELECT "posts".* FROM (
  SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" = 1
  UNION
  SELECT "posts".* FROM "posts"  WHERE (published_at < '2014-07-19 16:04:21.918366')
) posts

1

我只要运行您需要的两个查询,然后合并返回的记录数组即可:

@posts = watched_news_posts + watched_topics_posts

或者,至少进行测试。您认为红宝石中的阵列组合会太慢吗?查看建议的查询来解决问题,我不认为会有如此大的性能差异。


实际上,使用@ posts = watched_news_posts和watched_topics_posts可能会更好,因为它是一个交叉点,可以避免重复。
Jeffrey Alan Lee

1
我的印象是ActiveRelation懒惰地加载其记录。如果在Ruby中将数组相交,会不会丢失它?
LandonSchropp 2012年

显然,它返回的关系的工会下轨dev的,但我不知道它会在什么版本。
杰弗里·艾伦·李

1
与此返回数组相反,它的两个不同的查询结果合并在一起。
alexzg 2014年

1

在类似的情况下,我求和两个数组并使用Kaminari:paginate_array()。非常好,可行的解决方案。我无法使用where(),因为我需要order()在同一张表上对两个不同的结果求和。


1

问题更少,更容易遵循:

    def union_scope(*scopes)
      scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) }
    end

所以最后:

union_scope(watched_news_posts, watched_topic_posts)

1
我将其略微更改为:scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }Thx!
eikes

0

埃利奥特·纳尔逊(Elliot Nelson)回答很好,除了某些关系是空的情况。我会做这样的事情:

def union_2_relations(relation1,relation2)
sql = ""
if relation1.any? && relation2.any?
  sql = "(#{relation1.to_sql}) UNION (#{relation2.to_sql}) as #{relation1.klass.table_name}"
elsif relation1.any?
  sql = relation1.to_sql
elsif relation2.any?
  sql = relation2.to_sql
end
relation1.klass.from(sql)

结束


0

这是我如何在自己的ruby on rails应用程序上使用UNION加入SQL查询的方式。

您可以将以下内容用作自己的代码的灵感。

class Preference < ApplicationRecord
  scope :for, ->(object) { where(preferenceable: object) }
end

下面是我将合并范围合并在一起的UNION。

  def zone_preferences
    zone = Zone.find params[:zone_id]
    zone_sql = Preference.for(zone).to_sql
    region_sql = Preference.for(zone.region).to_sql
    operator_sql = Preference.for(Operator.current).to_sql

    Preference.from("(#{zone_sql} UNION #{region_sql} UNION #{operator_sql}) AS preferences")
  end
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.