2014-07-23 6 views
15

같은 테이블에 여러 개의 조인이있는 경우 ActiveRecord가 별칭 테이블 이름을 설정하는 상황이 있습니다. 이러한 조인에 범위가 포함되어있는 상황 ('병합'사용)에 갇혀 있습니다.동일한 테이블을 조건과 함께 두 번 결합하십시오.

모델 TABLE_NAME : users

두 번째 모델 TABLE_NAME : posts

가입 테이블 이름 : access_levels

게시물

나는 대다 관계를 가지고 access_levels를 통해 많은 사용자가 있고 그 반대도 마찬가지입니다.

, 사용자 모델과 포스트 모델 공유 같은 관계 :

has_many :access_levels, -> { merge(AccessLevel.valid) }

ACCESSLEVEL 모델의 내부 범위는 다음과 같습니다

# v1 
    scope :valid, -> { 
    where("(valid_from IS NULL OR valid_from < :now) AND (valid_until IS NULL OR valid_until > :now)", :now => Time.zone.now) 
    } 

    # v2 
    # scope :valid, -> { 
    # where("(#{table_name}.valid_from IS NULL OR #{table_name}.valid_from < :now) AND (#{table_name}.valid_until IS NULL OR #{table_name}.valid_until > :now)", :now => Time.zone.now) 
    # } 

내가 전화를하고 싶습니다 이 같은 sth :

Post.joins(:access_levels).joins(:users).where (...) 

활성 Record는 두 번째 조인 ('access_levels_users')의 별칭을 만듭니다. 이 테이블 이름을 AccessLevel 모델의 '유효한'범위 내부에서 참조하려고합니다.

V1은 분명히 PG::AmbiguousColumn -Error를 생성합니다. V2를 사용하면 두 조건 앞에 access_levels. 접두어가 붙습니다. 이는 의미 상 잘못되었습니다.

# inside of a policy 
scope = Post. 
    joins(:access_levels). 
    where("access_levels.level" => 1, "access_levels.user_id" => current_user.id) 

# inside of my controller 
scope.joins(:users).select([ 
     Post.arel_table[Arel.star], 
     "hstore(array_agg(users.id::text), array_agg(users.email::text)) user_names" 
     ]).distinct.group("posts.id") 

생성 된 쿼리 (위에서 valid 범위 (V2)을 사용)과 같다 간체 :

SELECT "posts".*, hstore(array_agg(users.id::text), array_agg(users.email::text)) user_names 

    FROM "posts" 
    INNER JOIN "access_levels" ON "access_levels"."post_id" = "posts"."id" AND (("access_levels"."valid_from" IS NULL OR "access_levels"."valid_from" < '2014-07-24 05:38:09.274104') AND ("access_levels"."valid_until" IS NULL OR "access_levels"."valid_until" > '2014-07-24 05:38:09.274132')) 
    INNER JOIN "users" ON "users"."id" = "access_levels"."user_id" 
    INNER JOIN "access_levels" "access_levels_posts" ON "access_levels_posts"."post_id" = "posts"."id" AND (("access_levels"."valid_from" IS NULL OR "access_levels"."valid_from" < '2014-07-24 05:38:09.274675') AND ("access_levels"."valid_until" IS NULL OR "access_levels"."valid_until" > '2014-07-24 05:38:09.274688')) 

    WHERE "posts"."deleted_at" IS NULL AND "access_levels"."level" = 4 AND "access_levels"."user_id" = 1 GROUP BY posts.id 

액티브이 방법은 적절한 별명 설정

이것은 I 쿼리를 생성하는 방법이다 access_levels 테이블의 두 x 째 조인에 대한 'access_levels_posts'. 문제는 병합 된 valid -scope가 'access_levels_posts'대신 'access_levels'로 열 앞에 붙는 것입니다. 또한 범위를 생성하기 위해 arel을 사용하려고했습니다.

# v3 
scope :valid, -> { 
    where arel_table[:valid_from].eq(nil).or(arel_table[:valid_from].lt(Time.zone.now)).and(
    arel_table[:valid_until].eq(nil).or(arel_table[:valid_until].gt(Time.zone.now)) 
) 
} 

결과 쿼리는 동일하게 유지됩니다.

+0

귀하의 질문에 약간 혼란 스럽지만 나는 당신이 무엇을하고 싶은지 알고 있습니다. 'valid '범위를'join (: user) .where ("(valid_from IS NULL 또는 valid_from <: now) AND (valid_until IS NULL 또는 valid_until> : 지금)", now : Time.zone.now) .where로 변경하십시오 (사용자 : {active : true, 또는 : something})' – jvnill

답변

2

나는 그동안 내 자신의 문제를 해결할 수있었습니다. 비슷한 문제가있는 다른 사람들을 돕기 위해 내 솔루션을 게시 할 것입니다.

서문 : 그것은 약속의 땅에 긴 방법) 나는 가능한 한 짧게 설정하겠습니다

:

원래 목표는 순서로 뭔가를 (전화를했다
# 
# Setup 
# 
class Post < ActiveRecord::Base 
    has_many :access_levels, -> { merge(AccessLevel.valid) } 
    has_many :users, :through => :access_levels 
end 

class AccessLevel < ActiveRecord::Base 
    belongs_to :post 
    belongs_to :user 

    scope :valid, -> { 
    where arel_table[:valid_from].eq(nil).or(arel_table[:valid_from].lt(Time.zone.now)).and(
     arel_table[:valid_until].eq(nil).or(arel_table[:valid_until].gt(Time.zone.now)) 
    ) 
    } 

    enum :level => [:publisher, :subscriber] 
end 

class User < ActiveRecord::Base 
    has_many :access_levels, -> { merge(AccessLevel.valid) } 
    has_many :users, :through => :access_levels 
end 

조건 추가 등.) : 의미 잘못된 쿼리 결과

Post.joins(:users).joins(:access_levels) 

:

SELECT "posts".* FROM "posts" 
    INNER JOIN "access_levels" 
    ON "access_levels"."post_id" = "posts"."id" 
     AND (("access_levels"."valid_from" IS NULL OR "access_levels"."valid_from" < '2014-09-15 20:42:46.835548') 
     AND ("access_levels"."valid_until" IS NULL OR "access_levels"."valid_until" > '2014-09-15 20:42:46.835688')) 

    INNER JOIN "users" 
    ON "users"."id" = "access_levels"."user_id" 

    INNER JOIN "access_levels" "access_levels_posts" 
    ON "access_levels_posts"."post_id" = "posts"."id" 
     AND (("access_levels"."valid_from" IS NULL OR "access_levels"."valid_from" < '2014-09-15 20:42:46.836090') 
     AND ("access_levels"."valid_until" IS NULL OR "access_levels"."valid_until" > '2014-09-15 20:42:46.836163')) 

두 번째 조인 별칭을 사용 -하지만 조건이 별칭을 사용하지 않습니다.

구조 대원들!

ActiveRecord를 신뢰하는 대신 베어 링으로 다음 조인을 모두 빌드했습니다. 불행히도 두 가지를 결합하는 것이 항상 예상대로 작동하지 않는 것 같습니다. 적어도 그런 식으로 작동합니다. 나는이 예제에서 외부 조인을 사용하고 있으므로 어쨌든 직접 빌드해야합니다. 또한 모든 쿼리는 Pundit을 사용하여 정책 내에 저장됩니다. 그래서 그들은 쉽게 테스트 할 수 있으며 뚱뚱한 컨트롤러 나 중복성이 없습니다. 그래서 나는 약간의 여분의 코드로 괜찮습니다.

SELECT "posts".* FROM "posts" 
    LEFT OUTER JOIN "access_levels" 
    ON "posts"."id" = "access_levels"."post_id" 
     AND ("access_levels"."valid_from" IS NULL OR "access_levels"."valid_from" < '2014-09-15 20:35:34.420077') 
     AND ("access_levels"."valid_until" IS NULL OR "access_levels"."valid_until" > '2014-09-15 20:35:34.420189') 

    LEFT OUTER JOIN "users" 
    ON "access_levels"."user_id" = "users"."id" 

    LEFT OUTER JOIN "access_levels" "al" 
    ON "posts"."id" = "al"."post_id" 
    AND ("al"."valid_from" IS NULL OR "al"."valid_from" < '2014-09-15 20:35:41.678492') 
    AND ("al"."valid_until" IS NULL OR "al"."valid_until" > '2014-09-15 20:35:41.678603') 

모든 조건이 지금은 적절한 별칭을 사용하고 있습니다 : 모든 피와 땀과 눈물 후

# 
# Our starting point ;) 
# 
scope = Post 

# 
# Rebuild `scope.joins(:users)` or `scope.joins(:access_levels => :user)` 
# No magic here. 
# 
join = Post.arel_table.join(AccessLevel.arel_table, Arel::Nodes::OuterJoin).on(
    Post.arel_table[:id].eq(AccessLevel.arel_table[:post_id]). 
    and(AccessLevel.valid.where_values) 
).join_sources 
scope = scope.joins(join) 

join = AccessLevel.arel_table.join(User.arel_table, Arel::Nodes::OuterJoin).on(
    AccessLevel.arel_table[:user_id].eq(User.arel_table[:id]) 
).join_sources 

scope = scope.joins(join) 

# 
# Now let's join the access_levels table for a second time while reusing the AccessLevel.valid scope. 
# To accomplish that, we temporarily swap AccessLevel.table_name 
# 
table_alias   = 'al'       # This will be the alias 
temporary_table_name = AccessLevel.table_name   # We want to restore the original table_name later 
AccessLevel.table_name = table_alias     # Set the alias as the table_name 
valid_clause   = AccessLevel.valid.where_values # Store the condition with our temporarily table_name 
AccessLevel.table_name = temporary_table_name   # Restore the original table_name 

# 
# We're now able to use the table_alias combined with our valid_clause 
# 
join = Post.arel_table.join(AccessLevel.arel_table.alias(table_alias), Arel::Nodes::OuterJoin).on(
    Post.arel_table[:id].eq(AccessLevel.arel_table.alias(table_alias)[:post_id]). 
    and(valid_clause) 
).join_sources 

scope = scope.joins(join) 

, 여기에 우리의 결과 쿼리입니다!

6

similar question here에서이 문제를 자세히 살펴본 후에이 문제에 대한 더 단순하고 청결한 해결책을 생각해 냈습니다. 귀하의 범위와 함께 완성을 위해 다른 질문에 대한 제 대답의 관련 비트를 붙여 넣으려고합니다.

요점은 현재 사용중인 경우 해당 table_aliases과 함께 arel_table 개체를 액세스 할 수있는 방법을 찾고 해당 실행 범위의 범위 내에서 발견하는 것이 었습니다. 이 테이블을 사용하면 테이블 이름에 별명이 지정된 JOIN (동일한 테이블의 다중 조인) 또는 범위에 테이블 이름에 대한 별명이없는 경우 스코프가 사용되고 있는지 알 수 있습니다.

# based on your v2 
scope :valid, -> { 
    where("(#{current_table_from_scope}.valid_from IS NULL OR 
      #{current_table_from_scope}.valid_from < :now) AND 
     (#{current_table_from_scope}.valid_until IS NULL OR 
      #{current_table_from_scope}.valid_until > :now)", 
     :now => Time.zone.now) 
    } 

def self.current_table_from_scope 
    current_table = current_scope.arel.source.left 

    case current_table 
    when Arel::Table 
    current_table.name 
    when Arel::Nodes::TableAlias 
    current_table.right 
    else 
    fail 
    end 
end 

내가 대신 self.class.arel_table 또는 relation.arel_table를 사용하는 이전의 시도는 arel 테이블을 찾기 위해 기본 개체로 current_scope를 사용하고 있습니다. 그 객체에 source을 호출하여 Arel::SelectManager을 얻습니다. 그러면 #left에있는 현재 테이블이 나타납니다. 현재로서는 Arel::Table (별칭 없음, 테이블 이름은 #name)이거나 #right에 별칭이있는 Arel::Nodes::TableAlias이라는 두 가지 옵션이 있습니다. 당신이 관심이 있다면

, 여기에 내가이 길을 사용하는 일부 참조는 다음과 같습니다

similar question on SO
  • , 당신 대신 당신의 아름다운하고 간결한 기능으로 사용할 수, 코드의 톤으로 대답했다.
  • Rails issue 및이 other one입니다.
+0

정말 멋진 솔루션처럼 보입니다. 확실히 시도해 보겠습니다. – jack

3

이와 같은 문제를 검색 할 때 나는이 질문을 발견했습니다. 나는 그것이 늦은 대답 인 것을 안다. 그러나 누군가 다른 사람이 여기에 아마도 넘어지면 아마도 도움이 될 수있다. 이 기능은 Rails 4.2.2에서 작동합니다. 질문이있을 때이 작업을 수행하지 못했을 수 있습니다.

이 답변은 @dgilperez의 답변에서 영감을 얻었지만 약간 단순화되었습니다. 또한 올바른 범위를 사용합니다. 그래서 여기 있습니다.

Post.includes(:users, :access_levels).references(:users, :access_levels).first 

그러나주의 :

class Post < ActiveRecord::Base 
    # the scope of the used association must be used 
    has_many :access_levels, -> { merge(AccessLevel.valid(current_scope)) } 
    has_many :users, :through => :access_levels 
end 

class AccessLevel < ActiveRecord::Base 
    belongs_to :post 
    belongs_to :user 

    # have an optional parameter for another scope than the scope of this class 
    scope :valid, ->(cur_scope = nil) { 
    # 'current_scope.table' is the same as 'current_scope.arel.source.left', 
    # and there is no need to investigate if it's an alias or not. 
    ar_table = cur_scope && cur_scope.table || arel_table 
    now = Time.zone.now 
    where(
     ar_table[:valid_from].eq(nil).or(ar_table[:valid_from].lt(now)).and(
     ar_table[:valid_until].eq(nil).or(ar_table[:valid_until].gt(now))) 
    ) 
    } 

    enum :level => [:publisher, :subscriber] 
end 

class User < ActiveRecord::Base 
    # the scope of the used association must be used 
    has_many :access_levels, -> { merge(AccessLevel.valid(current_scope)) } 
    has_many :users, :through => :access_levels 
end 

그리고 두 그것을 가지고 필요가 없습니다 내가, 당신이 그와 함께 얻을 수 있습니다 당신은 또한 외부 조인 사용하여 변경할 것을보고

Post.joins(:users, :access_levels).first 

조인 includes을 사용하는 것이 항상 하나의 SQL 요청을 사용하는 것은 아닙니다.

+0

정말 대단합니다! 실제 Arel 테이블 대신 테이블 이름 (문자열)을 원했기 때문에'(current_scope? current_scope.table : arel_table) .name'으로 끝났습니다. – Nathan

관련 문제