ActiveRecord Query Union - PullRequest
       33

ActiveRecord Query Union

81 голосов
/ 14 июля 2011

Я написал пару сложных запросов (по крайней мере для меня) с интерфейсом запросов 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?

Ответы [ 12 ]

84 голосов
/ 14 марта 2013

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

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

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

Я все равно был бы признателен, если у кого-то есть какие-либо предложениячтобы оптимизировать это или улучшить производительность, потому что он по сути выполняет три запроса и чувствует себя немного избыточным.

8 голосов
/ 12 марта 2014

Как насчет ...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end
7 голосов
/ 01 марта 2015

Вы также можете использовать Brian Hempel 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 когда-нибудь.

6 голосов
/ 15 июля 2011

Не могли бы вы использовать ИЛИ вместо СОЮЗА?

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

Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)

(Поскольку вы присоединяетесь к просматриваемой таблице дважды, я не слишком уверен, какие имена таблиц будут для запроса)

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

5 голосов
/ 10 августа 2011

Возможно, это улучшает читаемость, но не обязательно производительность:

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")
2 голосов
/ 28 января 2016

Существует камень 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

1 голос
/ 11 января 2013

В аналогичном случае я суммировал два массива и использовал Kaminari:paginate_array().Очень хорошее и рабочее решение.Я не смог использовать where(), потому что мне нужно сложить два результата с разными order() в одной таблице.

1 голос
/ 09 августа 2012

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

@posts = watched_news_posts + watched_topics_posts

Или, по крайней мере, проверить это. Как вы думаете, комбинация массивов в ruby ​​будет слишком медленной? Глядя на предлагаемые запросы, чтобы обойти проблему, я не уверен, что будет существенное различие в производительности.

...