ActiveRecord 쿼리 통합


90

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


scope 를 사용할 수 있어야합니다 . 2 개의 범위를 만든 다음 둘 다 Post.watched_news_posts.watched_topic_posts. 당신은 같은 것들의 범위에 PARAMS에 전송해야 :user_id하고 :topic.
Zabba

6
제안 해 주셔서 감사합니다. 문서에 따르면 "범위는 데이터베이스 쿼리의 축소를 나타냅니다". 제 경우에는 watched_news_posts와 watched_topic_posts 모두에있는 게시물을 찾고 있지 않습니다. 오히려 중복이 허용되지 않는 watched_news_posts 또는 watched_topic_posts에있는 게시물을 찾고 있습니다. 여전히 범위를 사용하여 수행 할 수 있습니까?
LandonSchropp 2011

1
즉시 사용할 수는 없습니다. github에는 union이라는 플러그인이 있지만 구식 구문 (클래스 메서드 및 해시 스타일 쿼리 매개 변수)을 사용합니다. 만약 그게 멋지다면 함께하겠습니다 ... 그렇지 않으면 먼 길을 씁니다. 범위에서 find_by_sql.
jenjenut233

1
나는 jenjenut233에 동의하며, 당신이 find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}"). 나는 그것을 테스트하지 않았으므로 시도하면 어떻게되는지 알려주십시오. 또한 작동하는 일부 ARel 기능이있을 수 있습니다.
OGZ의 마법사

2
나는 쿼리를 SQL 쿼리로 다시 작성했습니다. 지금은 작동하지만 불행히도 find_by_sql다른 연결 가능한 쿼리와 함께 사용할 수 없습니다. 즉, 이제 will_paginate 필터와 쿼리도 다시 작성해야합니다. ActiveRecord가 union작업을 지원하지 않는 이유는 무엇 입니까?
LandonSchropp 2011

답변:


93

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

2
이것은 위에 나열된 다른 것보다 더 완전한 대답입니다. 잘 작동합니다!
ghayes

사용 예가 좋을 것입니다.
ciembor

요청에 따라 예제를 추가했습니다.
팀 Lowrimore

3
해결책은 "거의"정확하고 +1을 주었지만 여기에서 수정 한 문제가 발생했습니다. gist.github.com/lsiden/260167a4d3574a580d97
Lawrence I. Siden

7
빠른 경고 :이 방법은 MySQL의 성능 측면에서 볼 때 매우 문제가 있습니다. 하위 쿼리가 테이블의 각 레코드에 대해 종속 된 것으로 간주되고 실행되기 때문입니다 ( percona.com/blog/2010/10/25/mysql-limitations-part 참조) . -3- 하위 쿼리 ).
shosti

71

나는 또한이 문제에 직면했고 이제 나의 전략은 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")

두 개의 다른 모델이 있다고 가정하면 unoin에 대한 쿼리가 무엇인지 알려주십시오.
Chitra

매우 유용한 답변입니다. 향후 독자를 위해 activerecord가 쿼리를 'SELECT "comments". "*"FROM "...으로 구성하므로 마지막"AS 주석 "부분을 기억하십시오. 통합 집합의 이름을 지정하지 않거나 다음과 같은 다른 이름을 지정하는 경우 "foo는 그대로"최종 SQL 실행이 실패합니다.
HeyZiko

1
이것이 바로 제가 찾던 것입니다. #orRails 4 프로젝트 를 지원하기 위해 ActiveRecord :: Relation을 확장 했습니다. 같은 모델을 가정 :klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
M. 와이어트

11

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)

기본적으로 세 개의 쿼리를 실행하고 약간 중복 된 느낌이 들기 때문에 누군가 이것을 최적화하거나 성능을 향상시킬 제안이 있다면 여전히 감사하겠습니다.


어떻게 이것으로 똑같은 일을 할 수 있습니까? gist.github.com/2241307 그래서 Array 클래스가 아닌 AR :: Relation 클래스를 생성합니까?
Marc

10

범위에 대한 메소드로 확장 되는 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언젠가 병합 될 것입니다.


8

어때 ...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end

15
pluck호출은 그 자체로 쿼리이므로 하나가 아닌 세 개의 쿼리를 수행 합니다.
JacobEvelyn 2014

3
이 배열을 반환하지 않습니다 그럼 당신이 사용할 수있는 렸기 때문에 이것은, 정말 좋은 솔루션 .order또는 .paginate방법을 그것은 ORM 클래스를 유지 ...
mariowise

범위가 동일한 모델 인 경우 유용하지만 뽑기 때문에 두 개의 쿼리가 생성됩니다.
jmjm

6

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

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

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

(당신이 감시 된 테이블을 두 번 조인하기 때문에 쿼리에 대한 테이블 이름이 무엇인지 잘 모르겠습니다)

조인이 많기 때문에 데이터베이스에서 상당히 무겁지만 최적화 할 수 있습니다.


2
너무 늦게 연락 드려서 미안하지만 지난 며칠 동안 휴가 중이 었어요. 내가 당신의 대답을 시도했을 때 내가 가진 문제는 조인 방법이 두 테이블을 조인하는 것이 아니라 비교할 수있는 두 개의 개별 쿼리가 아니라는 것입니다. 그러나 당신의 아이디어는 건전했고 나에게 또 다른 아이디어를주었습니다. 도와 주셔서 감사합니다.
LandonSchropp 2011

OR을 사용하여 선택하는 것이 UNION보다 느립니다. 대신 UNION에 대한 솔루션이 궁금합니다.
Nich

5

아마도 이것은 가독성을 향상 시키지만 반드시 성능을 향상시키는 것은 아닙니다.

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

posts.id는 어디서 얻나요?
berto77 2012-06-29

self.id는 SQL에서 두 번 참조되므로 두 개의 self.id 매개 변수가 있습니다. 두 개의 물음표를 참조하십시오.
richardsun

이것은 UNION 쿼리를 수행하고 ActiveRecord :: Relation을 반환하는 방법에 대한 유용한 예였습니다. 감사.
Fitter Man

이러한 유형의 SDL 쿼리를 생성 할 수있는 도구가 있습니까? 철자 오류 등없이 어떻게 수행 했습니까?
BKSpurgeon

2

active_record_union gem이 있습니다. 도움이 될 수 있습니다

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

필요한 두 쿼리를 실행하고 반환되는 레코드 배열을 결합합니다.

@posts = watched_news_posts + watched_topics_posts

또는 최소한 테스트 해보십시오. 루비의 배열 조합이 너무 느릴 것이라고 생각하십니까? 문제를 해결하기 위해 제안 된 쿼리를 살펴보면 성능 ​​차이가 크게 나타날 것이라고 확신하지 못합니다.


실제로 @ posts = watched_news_posts 및 watched_topics_posts를 수행하는 것이 교차로이기 때문에 더 좋을 수 있으며 속임수를 피할 수 있습니다.
Jeffrey Alan Lee

1
나는 ActiveRelation이 레코드를 느리게로드한다는 인상을 받았습니다. Ruby에서 배열을 교차하면 손실되지 않습니까?
LandonSchropp

분명히 관계를 반환하는 유니온은 레일에서 dev에 있지만 어떤 버전이 될지 모르겠습니다.
Jeffrey Alan Lee

1
이 반환 배열 대신 두 개의 다른 쿼리 결과가 병합됩니다.
alexzg 2014

1

비슷한 경우에 두 개의 배열을 합산하고 Kaminari:paginate_array(). 매우 훌륭하고 작동하는 솔루션. 동일한 테이블에서 where()서로 다른 두 결과를 합산해야하므로을 사용할 수 없습니다 order().


1

문제가 적고 따르기 쉽습니다.

    def union_scope(*scopes)
      scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) }
    end

그래서 결국 :

union_scope(watched_news_posts, watched_topic_posts)

1
나는 그것을 약간 변경했다 : scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }Thx!
eikes

0

Elliot Nelson은 일부 관계가 비어있는 경우를 제외하고는 좋은 대답을했습니다. 나는 다음과 같이 할 것입니다.

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)

종료


0

내 자신의 Ruby on Rails 애플리케이션에서 UNION을 사용하여 SQL 쿼리를 조인하는 방법은 다음과 같습니다.

아래 내용을 자신의 코드에 대한 영감으로 사용할 수 있습니다.

class Preference < ApplicationRecord
  scope :for, ->(object) { where(preferenceable: object) }
end

아래는 내가 스코프를 결합한 UNION입니다.

  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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.