Объединение запросов 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?
Ответы
Ответ 1
Вот небольшой небольшой модуль, который я написал, что позволяет использовать несколько областей 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
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
Ответ 2
Я также столкнулся с этой проблемой, и теперь моя стратегия заключается в том, чтобы генерировать 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")
Ответ 3
Основываясь на ответе Оливки, я придумал другое решение этой проблемы. Он немного похож на взломанный, но возвращает экземпляр 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)
Я по-прежнему буду признателен, если у кого-нибудь есть предложения по оптимизации или улучшению производительности, потому что он по существу выполняет три запроса и чувствует себя немного избыточным.
Ответ 4
Как насчет...
def union(scope1, scope2)
ids = scope1.pluck(:id) + scope2.pluck(:id)
where(id: ids.uniq)
end
Ответ 5
Вы также можете использовать Brian Hempel active_record_union gem, который расширяет 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
.
Ответ 6
Не могли бы вы использовать OR вместо UNION?
Тогда вы можете сделать что-то вроде:
Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)
(Поскольку вы дважды присоединяетесь к просмотренной таблице, я не слишком уверен, какие имена таблиц будут для запроса)
Поскольку существует много объединений, это может также быть довольно тяжелым для базы данных, но может быть оптимизировано.
Ответ 7
Возможно, это улучшает читаемость, но не обязательно производительность:
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")
Ответ 8
Я бы просто выполнил два требуемых запроса и объединил массивы возвращаемых записей:
@posts = watched_news_posts + watched_topics_posts
Или, по крайней мере, проверьте это. Считаете ли вы, что комбинация массива в рубине будет слишком медленной? Рассматривая предлагаемые запросы, чтобы обойти эту проблему, я не уверен, что будет значительная разница в производительности.
Ответ 9
В аналогичном случае я суммировал два массива и использовал Kaminari:paginate_array()
. Очень приятное и рабочее решение. Мне не удалось использовать where()
, потому что мне нужно суммировать два результата с разными order()
в одной таблице.
Ответ 10
Существует массив 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