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のクエリインターフェースで同様のことができるかどうかは誰にもわかりますか?
これは、複数のスコープを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
私もこの問題に遭遇しました、そして今、私の行くべき戦略は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")
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つのクエリを実行しており、少し冗長であるため、これを最適化するか、パフォーマンスを改善するための提案があれば、まだ感謝しています。
どうですか...
def union(scope1, scope2)
ids = scope1.pluck(:id) + scope2.pluck(:id)
where(id: ids.uniq)
end
また、スコープのActiveRecord
メソッドでunion
を拡張する Brian Hempel の active_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
にマージされることを願っています。
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回参加しているため、クエリのテーブル名がどうなるかわからない)
多くの結合があるため、データベースにかなりの負荷がかかる可能性がありますが、最適化できる可能性があります。
おそらく、これにより可読性は向上しますが、必ずしもパフォーマンスは向上しません。
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")
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つのクエリを実行し、返されるレコードの配列を結合するだけです。
@posts = watched_news_posts + watched_topics_posts
または、少なくともテストしてみてください。 Rubyの配列の組み合わせは非常に遅いと思いますか?問題を回避するために提案されたクエリを見て、パフォーマンスに大きな違いがあると確信していません。
同様の場合、2つの配列を合計し、Kaminari:paginate_array()
を使用しました。とても素敵で実用的なソリューション。同じテーブルで異なるwhere()
と2つの結果を合計する必要があるため、order()
を使用できませんでした。
エリオット・ネルソンは、いくつかの関係が空である場合を除き、良いと答えました。私はそのようなことをします:
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)
終わり
自分で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