Ale*_*lex 16 sql arel ruby-on-rails-4 rails-activerecord
如果存在多个具有相同表的联接,则有些情况下ActiveRecord会设置别名表名.我陷入了这些连接包含范围的情况(使用'merge').
我有多对多的关系:
型号table_name:
users第二个模型table_name:
posts加入表名:
access_levels
帖子通过access_levels有很多用户,反之亦然.
User模型和Post模型共享相同的关系:
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)
# }
Run Code Online (Sandbox Code Playgroud)
我想这样打电话:
Post.joins(:access_levels).joins(:users).where (...)
Run Code Online (Sandbox Code Playgroud)
ActiveRecord为第二个连接创建别名('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")
Run Code Online (Sandbox Code Playgroud)
生成的查询如下所示(使用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
Run Code Online (Sandbox Code Playgroud)
ActiveRecord为access_levels表的第二个连接设置一个专用别名'access_levels_posts'.问题是合并的valid-scope在列前面加上'access_levels'而不是'access_levels_posts'.我还尝试使用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))
)
}
Run Code Online (Sandbox Code Playgroud)
生成的查询保持不变.
在这里对类似问题仔细研究这个问题之后,我想出了一个更简单,更清洁(在我看来)解决这个问题的方法.我在这里粘贴了我对另一个问题的答案的相关部分,以及完整性,以及你的范围.
关键是要找到一种方法来访问当前arel_table对象,table_aliases如果它们正被使用,则在执行时在范围内.使用该表,您将能够知道范围是否在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
Run Code Online (Sandbox Code Playgroud)
我正在使用current_scope作为基础对象来查找arel表,而不是之前尝试使用self.class.arel_table甚至是relation.arel_table.我正在呼吁source那个对象获得一个Arel::SelectManager反过来会给你当前的表#left.此时有两个选项:你有一个Arel::Table(没有别名,表名是打开的#name)或者你有一个Arel::Nodes::TableAlias别名#right.
如果你有兴趣,这里有一些我在路上使用的参考:
与此同时,我已经能够解决自己的问题了。我将发布我的解决方案以帮助其他遇到类似问题的人。
序言:通往应许之地还有很长的路要走;)
我会让设置尽可能短:
#
# 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
Run Code Online (Sandbox Code Playgroud)
最初的目标是调用类似的东西(为了添加更多条件等):
Post.joins(:users).joins(:access_levels)
Run Code Online (Sandbox Code Playgroud)
这会导致语义错误的查询:
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'))
Run Code Online (Sandbox Code Playgroud)
第二个连接使用别名 - 但条件不使用此别名。
阿瑞尔来救援!
我使用裸露的 arel 构建了以下所有连接,而不是信任 ActiveRecord。不幸的是,将两者结合起来似乎并不总是能按预期工作。但至少它是这样运作的。我在这个例子中使用了外连接,所以无论如何我都必须自己构建它们。此外,所有这些查询都存储在策略内(使用 Pundit)。因此它们很容易测试,并且没有胖控制器或任何冗余。所以我可以接受一些额外的代码。
#
# 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)
Run Code Online (Sandbox Code Playgroud)
经过所有的血、汗和泪水之后,这是我们得到的查询:
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')
Run Code Online (Sandbox Code Playgroud)
所有条件现在都使用正确的别名!
| 归档时间: |
|
| 查看次数: |
5890 次 |
| 最近记录: |