web-dev-qa-db-ja.com

ActiveRecord Query Union

RailのクエリインターフェイスでRubyを使用して、少なくとも2つの複雑なクエリを作成しました。

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オブジェクトを返します。これらの投稿を1つのActiveRelationにまとめたいと思います。ある時点で数十万の投稿がある可能性があるため、データベースレベルでこれを行う必要があります。 MySQLクエリの場合、UNION演算子を使用できます。 RoRのクエリインターフェースで同様のことができるかどうかは誰にもわかりますか?

81
LandonSchropp

これは、複数のスコープを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
84
Tim Lowrimore

私もこの問題に遭遇しました、そして今、私の行くべき戦略は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")

2つの異なるモデルでもこれを行うことができますが、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")
55
Elliot Nelson

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)

基本的に3つのクエリを実行しており、少し冗長であるため、これを最適化するか、パフォーマンスを改善するための提案があれば、まだ感謝しています。

11
LandonSchropp

どうですか...

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

また、スコープのActiveRecordメソッドでunionを拡張する Brian Hempelactive_record_union gemを使用することもできます。

クエリは次のようになります。

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

いつかこれが最終的にActiveRecordにマージされることを願っています。

7
dgilperez

UNIONの代わりにORを使用できますか?

その後、次のようなことができます:

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

(監視対象のテーブルに2回参加しているため、クエリのテーブル名がどうなるかわからない)

多くの結合があるため、データベースにかなりの負荷がかかる可能性がありますが、最適化できる可能性があります。

6
Olives

おそらく、これにより可読性は向上しますが、必ずしもパフォーマンスは向上しません。

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")
5
richardsun

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

2
Mike Lyubarskyy

必要な2つのクエリを実行し、返されるレコードの配列を結合するだけです。

@posts = watched_news_posts + watched_topics_posts

または、少なくともテストしてみてください。 Rubyの配列の組み合わせは非常に遅いと思いますか?問題を回避するために提案されたクエリを見て、パフォーマンスに大きな違いがあると確信していません。

1

同様の場合、2つの配列を合計し、Kaminari:paginate_array()を使用しました。とても素敵で実用的なソリューション。同じテーブルで異なるwhere()と2つの結果を合計する必要があるため、order()を使用できませんでした。

1
sekrett

エリオット・ネルソンは、いくつかの関係が空である場合を除き、良いと答えました。私はそのようなことをします:

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
Ehud

自分でUNIONを使用してSQLクエリを結合する方法は次のとおりです。Ruby on Rails application。

独自のコードのインスピレーションとして以下を使用できます。

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
0
joeyk16