101

我用 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 的查询界面做类似的事情吗?

4

14 回答 14

105

这是我编写的一个快速的小模块,它允许您联合多个范围。它还将结果作为 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
于 2013-03-14T15:35:34.887 回答
77

我也遇到过这个问题,现在我的首选策略是生成 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")
于 2013-06-01T03:19:26.103 回答
11

根据 Olives 的回答,我确实想出了另一个解决这个问题的方法。感觉有点像 hack,但它返回一个 的实例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)

如果有人对优化或提高性能有任何建议,我仍然会很感激,因为它本质上是在执行三个查询,感觉有点多余。

于 2011-07-18T23:49:02.447 回答
10

您还可以使用Brian Hempelactive_record_union gem,它ActiveRecord使用union范围方法进行扩展。

您的查询将是这样的:

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

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

于 2015-03-01T18:05:33.843 回答
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)

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

由于有很多连接,它可能对数据库也很重,但它可能能够被优化。

于 2011-07-15T17:51:35.393 回答
6

怎么样...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end
于 2014-03-12T01:17:10.753 回答
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")
于 2011-08-10T05:43:09.410 回答
2

有一个 active_record_union gem。可能会有所帮助

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
于 2016-01-28T18:13:23.077 回答
1

在类似的情况下,我将两个数组相加并使用Kaminari:paginate_array(). 非常好的和有效的解决方案。我无法使用where(),因为我需要order()在同一张桌子上总结两个不同的结果。

于 2013-01-11T14:31:47.093 回答
1

下面是我在自己的 ruby​​ on rails 应用程序上使用 UNION 加入 SQL 查询的方法。

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

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

下面是我加入范围的联合。

  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
于 2019-05-29T06:20:37.030 回答
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)
于 2019-12-17T12:40:54.800 回答
0

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

@posts = watched_news_posts + watched_topics_posts

或者,至少测试一下。你认为 ruby​​ 中的数组组合会太慢吗?查看建议的查询以解决该问题,我不相信会有那么显着的性能差异。

于 2012-08-09T05:16:50.880 回答
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)

结尾

于 2018-02-21T11:56:08.430 回答
0

当我们将 UNION 添加到范围时,由于在 UNION 之前添加了 order_by 子句,它有时会中断。

所以我以某种方式对其进行了更改,以使其具有 UNION 效果。

module 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.pluck(:id) }.flatten
      where("#{id_column} IN (?)", sub_query)
    end
  end
end

然后在任何模型中像这样使用它

class Model
  include UnionScope
  scope :union_of_scopeA_scopeB, -> { union_scope(scopeA, scopeB) }
end
于 2022-02-08T12:02:26.777 回答