[ruby-on-rails] 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의 쿼리 인터페이스와 비슷한 작업을 수행 할 수 있는지 아는 사람이 있습니까?



답변

다음은 여러 범위를 통합 할 수있는 간단한 모듈입니다. 또한 결과를 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")


답변

Olives의 대답을 바탕으로이 문제에 대한 또 다른 해결책을 찾았습니다. 약간 해킹처럼 느껴지지만 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 Hempelactive_record_union gem을 사용할 수도 있습니다 .ActiveRecordunion

쿼리는 다음과 같습니다.

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


답변

UNION 대신 OR를 사용할 수 있습니까?

그런 다음 다음과 같이 할 수 있습니다.

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")