Объединение Запросов ActiveRecord
Я написал пару сложных запросов (по крайней мере, для меня) с интерфейсом запросов 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})
оба эти запроса прекрасно работают сами по себе. Обе функции возвращают объекты пост. Я хотел бы объединить эти посты в единую активную деятельность. Поскольку в какой-то момент могут быть сотни тысяч сообщений, это необходимо сделать на уровне базы данных. Если бы это был запрос MySQL, я мог бы просто использовать UNION
оператора. Кто-нибудь знает, могу ли я что-то сделать аналогично с интерфейсом запроса RoR?
11 ответов:
вот быстрый маленький модуль, который я написал, что позволяет объединить несколько областей. Он также возвращает результаты как экземпляр 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
Edit:
как и просили, вот пример того, как 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")
вы можете сделать это с двумя различными моделями, а также, но вы должны убедиться, что они оба "выглядят одинаково" внутри союза - вы можете использовать
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")
основываясь на ответе оливок, я придумал другое решение этой проблемы. Это немного похоже на хак, но он возвращает экземпляр
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)
Я все равно буду признателен, если у кого-нибудь есть предложения по оптимизации этого или повышению производительности, потому что он по существу выполняет три запроса и чувствует себя немного избыточным.
Как насчет...
def union(scope1, scope2) ids = scope1.pluck(:id) + scope2.pluck(:id) where(id: ids.uniq) end
вы также можете использовать Брайан Хемпель ' s 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
какой-то день.
Не могли бы вы использовать или вместо союза?
тогда вы могли бы сделать что-то вроде:
Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched}) .where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)
(Так как вы присоединяетесь к наблюдаемой таблице дважды, я не слишком уверен, что имена таблиц будут для запроса)
поскольку существует много соединений, он также может быть довольно тяжелым для базы данных, но его можно оптимизировать.
возможно, это улучшает читаемость, но не обязательно производительность:
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::отношение, так что вы могли бы назвать его так:
my_posts.order("watched_item_type, post.id DESC")
есть драгоценный камень active_record_union. Может быть полезно
https://github.com/brianhempel/active_record_union
С ActiveRecordUnion, мы можем сделать:
сообщения текущего пользователя (черновик) и все опубликованные сообщения от кого-либо текущий пользователь.посты.Союз(пост.опубликованный) Что эквивалентно следующему 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
Я бы просто запустил два запроса, которые вам нужны, и объединил массивы возвращаемых записей:
@posts = watched_news_posts + watched_topics_posts
или, по крайней мере, проверить его. Как вы думаете, комбинация массивов в ruby будет слишком медленной? Глядя на предлагаемые запросы, чтобы обойти проблему, я не уверен, что будет такая значительная разница в производительности.
в аналогичном случае я суммировал два массива и использовал
Kaminari:paginate_array()
. Очень хорошее и рабочее решение. Я не смог использоватьwhere()
, потому что мне нужно суммировать два результата с разными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)
конец