Я написал пару сложных запросов (по крайней мере, для меня) с интерфейсом запросов 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?
ruby-on-rails
activerecord
union
active-relation
Лэндон Шропп
источник
источник
Post.watched_news_posts.watched_topic_posts
. Возможно, вам потребуется отправить параметры в области для таких вещей, как:user_id
и:topic
.find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")
. Я не проверял это, поэтому дайте мне знать, как это происходит, если вы попробуете. Кроме того, вероятно, есть некоторые функции ARel, которые будут работать.find_by_sql
не могут использоваться с другими цепочками запросов, а это означает, что теперь мне также нужно переписать мои фильтры и запросы will_paginate. Почему ActiveRecord не поддерживает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")
Вы также можете сделать это с двумя разными моделями, но вам нужно убедиться, что они обе «выглядят одинаково» внутри 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")
источник
#or
в моем проекте Rails 4. Предполагая ту же модель:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
Основываясь на ответе Олив, я придумал другое решение этой проблемы. Это немного похоже на взлом, но он возвращает экземпляр
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)
Я все равно был бы признателен, если бы у кого-нибудь были предложения по оптимизации или повышению производительности, потому что он, по сути, выполняет три запроса и кажется немного избыточным.
источник
Вы можете также использовать Brian Hempel «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
.источник
Как насчет...
def union(scope1, scope2) ids = scope1.pluck(:id) + scope2.pluck(:id) where(id: ids.uniq) end
источник
pluck
вызов сам по себе является запросом..order
or.paginate
... Оно сохраняет классыМогли бы вы использовать 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
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
Или, по крайней мере, проверить это. Как вы думаете, комбинация массивов в рубине будет слишком медленной? Глядя на предлагаемые запросы для решения проблемы, я не уверен, что разница в производительности будет такой значительной.
источник
В аналогичном случае я суммировал два массива и использовал
Kaminari:paginate_array()
. Очень красивое и рабочее решение. Я не смог использоватьwhere()
, потому что мне нужно суммировать два результата с разнымиorder()
в одной таблице.источник
Меньше проблем и легче следить:
def union_scope(*scopes) scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) } end
Итак, в итоге:
источник
scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }
Thx!Эллиот Нельсон ответил хорошо, за исключением случая, когда некоторые отношения пусты. Я бы сделал что-то подобное:
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)
конец
источник
Вот как я объединил SQL-запросы с помощью UNION в моем собственном приложении ruby on rails.
Вы можете использовать приведенное ниже как вдохновение для своего собственного кода.
class Preference < ApplicationRecord scope :for, ->(object) { where(preferenceable: object) } end
Ниже представлен СОЮЗ, в котором я объединил прицелы.
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
источник