Sql 使用条件连接同一个表两次
在某些情况下,如果同一个表有多个联接,ActiveRecord会设置别名表名。我陷入了这样一种情况:这些联接包含使用“merge”的作用域 我有一种多对多的关系: 模型表名称:用户 第二个模型表名称:posts 联接表名称:访问\u级别 一篇文章通过访问级别拥有许多用户,反之亦然 用户模型和后期模型共享相同的关系: 有多个:访问级别,->{mergeAccessLevel.valid} AccessLevel模型内部的作用域如下所示:Sql 使用条件连接同一个表两次,sql,ruby-on-rails-4,rails-activerecord,arel,Sql,Ruby On Rails 4,Rails Activerecord,Arel,在某些情况下,如果同一个表有多个联接,ActiveRecord会设置别名表名。我陷入了这样一种情况:这些联接包含使用“merge”的作用域 我有一种多对多的关系: 模型表名称:用户 第二个模型表名称:posts 联接表名称:访问\u级别 一篇文章通过访问级别拥有许多用户,反之亦然 用户模型和后期模型共享相同的关系: 有多个:访问级别,->{mergeAccessLevel.valid} AccessLevel模型内部的作用域如下所示: # v1 scope :valid, -> {
# 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)
# }
Post.joins(:access_levels).joins(:users).where (...)
ActiveRecord为第二个联接“访问\u级别\u用户”创建别名。我想在AccessLevel模型的“有效”范围内引用此表名
V1显然会生成PG::AmbiguousColumn错误。
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")
使用上面的有效范围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
结果查询保持不变。与此同时,我已经能够解决我自己的问题。我将发布我的解决方案,以帮助其他有类似问题的人 序言:这是一个漫长的道路,以承诺的土地 我将尽可能缩短设置时间:
#
# 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
这将导致语义错误的查询:
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'))
在经历了所有的鲜血、汗水和泪水之后,我们的疑问是:
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')
所有条件现在都使用正确的别名 在仔细研究了一个问题后,我想出了一个更简单、更清晰的解决方案。为了完整性,我将另一个问题的答案的相关部分粘贴在这里,以及您的范围 关键是找到一种方法来访问当前arel_table对象,如果正在使用它的table_别名,则在执行时在作用域内访问它。使用该表,您将能够知道该作用域是否正在表名为同一表上多个联接的别名的联接中使用,或者该作用域是否没有表名的别名
# 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
我使用作为基础对象来查找arel表,而不是以前尝试使用self.class.arel_表甚至relation.arel_表。我正在调用该对象的源以获取一个值,该值将依次为您提供左侧的当前表。此时有两个选项:一个是Arel::Table无别名,一个是name上的Table name,另一个是Arel::Nodes::TableAlias,别名在其右侧
如果您感兴趣,以下是我在这条路上使用的一些参考资料:
A,用大量的代码回答,你可以用它来代替你漂亮简洁的能力。
这个和这个。
我在寻找这样的东西时遇到了这个问题。我知道这是一个迟来的答案,但如果有人在这里绊倒了,也许这会有所帮助。这在Rails 4.2.2中是有效的,可能在提出问题时无法实现 这个答案的灵感来自@dgilperez的答案,但有点简化。也使用正确的范围。所以,在这里
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.includes(:users, :access_levels).references(:users, :access_levels).first
但是请注意,使用includes并不总是使用一个SQL请求。您的问题有点让人困惑,但我想我知道您想做什么。将有效范围更改为joins:user.wherevalid\u from为NULL或valid\u from<:now和valid\u until为NULL或valid\u until>:now,now:Time.zone.now.whereusers:{active:true,OR:something}看起来是一个非常好的解决方案-我一定会尝试一下这太棒了!我想要的是表名字符串而不是实际的Arel表,所以我最终得到了当前的\u范围?当前_scope.table:arel_table.name
# 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
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
Post.includes(:users, :access_levels).references(:users, :access_levels).first