Объединение запросов 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}) 

Оба этих запроса отлично работают сами по себе. Оба возвращают объекты Post. Я хотел бы объединить эти сообщения в один 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 

Я также столкнулся с этой проблемой, и теперь моя страtagsя заключается в том, чтобы генерировать 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") 

Вы можете сделать это и с двумя разными моделями, но вам нужно убедиться, что они оба «выглядят одинаково» внутри 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") 

Основываясь на ответе Оливки, я придумал другое решение этой проблемы. Он немного похож на хак, но он возвращает экземпляр 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 

Вы также можете использовать gem active_record_union от Brian Hempel, который расширяет 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 нибудь.

Не могли бы вы использовать OR вместо UNION?

Тогда вы можете сделать что-то вроде:

 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 :: Relation, поэтому вы можете называть его следующим образом:

 my_posts.order("watched_item_type, post.id DESC") 

Существует жемчужина active_record_union. Может быть полезно

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

Я бы просто запустил два запроса, которые вам нужны, и объединить массивы возвращаемых записей:

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

конец

Давайте будем гением компьютера.