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

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)

конец