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


スコープを使用できるはずです。2つのスコープを作成し、のように両方を呼び出しますPost.watched_news_posts.watched_topic_posts:user_idやなどのスコープにparamsを送信する必要がある場合があります:topic
Zabba

6
提案をありがとう。ドキュメントによると、「スコープはデータベースクエリの絞り込みを表します」。私の場合、watched_news_postsとwatched_topic_postsの両方にある投稿を探していません。むしろ、watched_news_postsまたはwatched_topic_postsにあり、重複が許可されていない投稿を探しています。これはまだスコープで達成できますか?
LandonSchropp、2011

1
すぐに使用できるわけではありません。githubにunionというプラグインがありますが、古い構文(クラスメソッドとハッシュスタイルのクエリパラメータ)を使用しています。スコープ内のfind_by_sql。
jenjenut233 11/07/14

1
私はjenjenut233に同意し、あなたはのようなことができると思いますfind_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")。まだテストしていませんので、試してみたらどうなるか教えてください。また、おそらく機能するいくつかのARel機能があるでしょう。
Wizard of Ogz、2011

2
クエリをSQLクエリとして書き直しました。現在は機能しますが、残念ながらfind_by_sql他の連鎖可能なクエリでは使用できません。つまり、will_paginateフィルターとクエリも書き換える必要があります。ActiveRecordがunion操作をサポートしないのはなぜですか?
LandonSchropp 2011

回答:


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 2013

使用例がいいでしょう。
ciembor 2014

リクエストに応じて、例を追加しました。
Tim Lowrimore 2014年

3
解決策は「ほぼ」正しく、+ 1を与えましたが、ここで修正した問題に遭遇しました:gist.github.com/lsiden/260167a4d3574a580d97
Lawrence I.

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

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

2つの異なるモデルがある場合は、unoinのクエリを教えてください。
Chitra

非常に役立つ答え。今後の読者のために、最後の「ASコメント」の部分を覚えておいてください。activerecordはクエリを「SELECT "comments"。 "*" FROM "...」として構成するためです。ユニオンセットの名前を指定しない場合や、次のような別の名前を指定する場合「AS foo」、最後のSQL実行は失敗します
HeyZiko

1
これはまさに私が探していたものでした。ActiveRecord :: Relationを拡張#orして、Rails 4プロジェクトでサポートできるようにしました。同じモデルを想定:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
M.ワイアット

11

オリーブの答えに基づいて、私はこの問題の別の解決策を考え出しました。それは少しハックのように感じますが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つのクエリを実行しており、少し冗長に感じられるため、これを最適化するか、パフォーマンスを改善するための提案があれば、私はそれでも感謝します。


これで同じことをするにはどうすればよいですか:gist.github.com/2241307 これにより、ArrayクラスではなくAR :: Relationクラスが作成されますか?
Marc

10

また、使用することができブライアン・ヘンペルさんactive_record_unionの拡張宝石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いつかマージされるでしょう。


8

どう...

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

15
pluck呼び出しはそれ自体がクエリであるため、1つではなく3つのクエリを実行することに注意してください。
JacobEvelyn 14

3
これは本当に良い解決策です。配列を返さないため、.orderまたは.paginateメソッドを使用できます... ormクラスを保持します
mariowise

スコープが同じモデルである場合に役立ちますが、これにより、むし歯のために2つのクエリが生成されます。
jmjm 2016年

6

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回参加しているので、クエリで使用するテーブルの名前はわかりません)

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


2
お返事が遅くなって申し訳ありませんが、ここ数日は休暇中です。私があなたの答えを試したときに私が抱えていた問題は、joinsメソッドが2つの別々のクエリを比較するのではなく、両方のテーブルを結合することでした。しかし、あなたの考えはしっかりしていて、私に別の考えを与えました。助けてくれてありがとう。
LandonSchropp、2011

ORを使用した選択はUNIONよりも遅く、代わりにUNIONの解決策はありません
Nich

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はSQLで2回参照されるため、2つのself.idパラメーターがあります。2つの疑問符を参照してください。
richardsun 2012年

これは、UNIONクエリを実行してActiveRecord :: Relationを取得する方法の便利な例です。ありがとう。
フィッターマン

これらのタイプのSDLクエリを生成するツールはありますか?スペルミスなどなく、どのように実行しましたか?
BKSpurgeon

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

1

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

@posts = watched_news_posts + watched_topics_posts

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


実際には@ posts = watched_news_posts&watched_topics_postsを実行する方が交差点であり、重複を回避できるため、より適切な場合があります。
ジェフリーアランリー

1
ActiveRelationがレコードを遅延読み込みするという印象を受けました。Rubyで配列と交差した場合、それを失うことはありませんか?
LandonSchropp 2012

どうやら関係を返し組合は、レールにdevの下にあり、私はそれがになりますどのバージョンかわからない。
ジェフリー・アラン・リー

1
この戻り配列ではなく、2つの異なるクエリ結果がマージされます。
alexzg 2014

1

同様の場合、2つの配列を合計して使用しましたKaminari:paginate_array()。とても素敵で実用的なソリューション。同じテーブルwhere()で異なる2つの結果を合計する必要があるため、を使用できませんでした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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.