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