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

4

14 に答える 14

105

これは、複数のスコープを 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
于 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")

これは 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")
于 2013-06-01T03:19:26.103 に答える
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)

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

于 2011-07-18T23:49:02.447 に答える
10

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

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

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

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回参加しているため、クエリのテーブルの名前がどうなるかわかりません)

結合が多いので、データベースにもかなり負担がかかるかもしれませんが、最適化できるかもしれません。

于 2011-07-15T17:51:35.393 に答える
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 に答える
1

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

于 2013-01-11T14:31:47.093 に答える
1

自分の 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
于 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

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

@posts = watched_news_posts + watched_topics_posts

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

于 2012-08-09T05:16:50.880 に答える