我想使用Squeel重构下面的查询。我想这样做,以便我可以链接其中的运算符并重新使用查询的不同部分中的逻辑。
User.find_by_sql("SELECT
users.*,
users.computed_metric,
users.age_in_seconds,
( users.computed_metric / age_in_seconds) as compound_computed_metric
from
(
select
users.*,
(users.id *2 ) as computed_metric,
(extract(epoch from now()) - extract(epoch from users.created_at) ) as age_in_seconds
from users
) as users")
查询必须全部在DB中运行,并且不应该是混合Ruby解决方案,因为它必须对数百万条记录进行排序和切片。
我已经设置了问题,因此它应该针对正常的user
表运行,以便您可以使用它的替代方案。
User
对象extra_metric_we_care_about
,age_in_seconds
和compound_computed_metric
下面的解决方案不起作用,但它显示了我希望实现的优雅类型
class User < ActiveRecord::Base
# this doesn't work - it just illustrates what I want to achieve
def self.w_all_additional_metrics
select{ ['*',
computed_metric,
age_in_seconds,
(computed_metric / age_in_seconds).as(compound_computed_metric)]
}.from{ User.w.compound_computed_metric.w_age_in_seconds }
end
def self.w_computed_metric
where{ '(id *2 ) as computed_metric' }
end
def self.w_age_in_seconds
where{ '(extract(epoch from now()) - extract(epoch from created_at) ) as age_in_seconds' }
end
end
请注意我已经设法解决了这个问题,以便您可以使用现有的User
课程并在控制台中使用它。
答案 0 :(得分:7)
我的案件中有2个苏打。我的数据库是mysql,我简化了你的demo代码,我想你可以扩展它。
第一种是Squeel方式,我在Squeel中混合了“sift”,在ActiveRecord Query中混合了“from”。 我刚刚安装了postgresql并测试了我的解决方案,似乎很难一起使用“squeel”和“epoch from”,但我在postgresql中找到了另一种方法,它叫做“date_part”。我还修改了sql并减少了重复计算:
class User < ActiveRecord::Base
sifter :w_computed_metric do
(id * 2).as(computed_metric)
end
sifter :w_age_in_seconds do
(date_part('epoch' , now.func) - date_part('epoch', created_at)).as(age_in_seconds)
end
sifter :w_compound_computed_metric do
(computed_metric / age_in_seconds).as(compound_computed_metric)
end
def self.subquery
select{['*', sift(w_computed_metric) , sift(w_age_in_seconds)]}
end
def self.w_all_additional_metrics
select{['*', sift(w_compound_computed_metric)]}.from("(#{subquery.to_sql}) users")
end
end
它产生了sql:
SELECT *, "users"."computed_metric" / "users"."age_in_seconds" AS compound_computed_metric
FROM (SELECT *,
"users"."id" * 2 AS computed_metric,
date_part('epoch', now()) - date_part('epoch', "users"."created_at") AS age_in_seconds FROM "users"
) users
您可以使用控制台进行测试:
> User.w_all_additional_metrics.first.computed_metric
=> "2"
> User.w_all_additional_metrics.first.age_in_seconds
=> "633.136693954468"
> User.w_all_additional_metrics.first.compound_computed_metric
=> "0.00315887551471441"
第二种是ActiveRecord方式,因为你的sql不是很复杂,所以你可以在ActiveRecord Query中链接它,对于一些范围就足够了:
class User < ActiveRecord::Base
scope :w_computed_metric, proc { select('id*2 as computed_metric') }
scope :w_age_in_seconds, proc { select('extract (epoch from (now()-created_at)) as age_in_seconds') }
scope :w_compound_computed_metric, proc { select('computed_metric/age_in_seconds as compound_computed_metric') }
def self.subquery
select('*').w_computed_metric.w_age_in_seconds
end
def self.w_all_additional_metrics
subquery.w_compound_computed_metric.from("(#{subquery.to_sql}) users")
end
end
这会生成以下SQL:
SELECT
*, id*2 as computed_metric,
extract (epoch from (now()-created_at)) as age_in_seconds,
computed_metric / age_in_seconds as compound_computed_metric
FROM (
SELECT
*,
id*2 as computed_metric,
extract (epoch from (now()-created_at)) as age_in_seconds
FROM
"users"
) users
ORDER BY compound_computed_metric DESC
LIMIT 1
希望它符合您的要求:)
答案 1 :(得分:2)
很可能我完全错了。我觉得你过分简化你的问题是为了让别人理解它。由于我无法在评论中提供格式良好的代码,因此我在此输入答案。
SELECT
users.*,
users.computed_metric,
users.age_in_seconds,
( users.computed_metric / age_in_seconds) as compound_computed_metric
from
(
select
users.*,
(users.id *2 ) as computed_metric,
(extract(epoch from now()) - extract(epoch from users.created_at) ) as age_in_seconds
from users
) as users
下面的SQL等同于上面的SQL。这就是为什么我说子查询不是必需的。
select
users.*,
(users.id *2 ) as computed_metric,
(extract(epoch from now()) - extract(epoch from users.created_at) ) as age_in_seconds,
computed_metric/age_in_seconds as compound_computed_metric
from users
如果这是正确的,则可以通过以下方式计算compound_computed_metric。无需自定义查询。
class User < ActiveRecord::Base
def compound_computed_metric
computed_metric/age_in_seconds
end
def computed_metric
self.id * 2
end
def age_in_seconds
Time.now - self.created_at
end
end
1.9.3p327 :001 > u = User.first
User Load (0.1ms) SELECT "users".* FROM "users" LIMIT 1
=> #<User id: 1, name: "spider", created_at: "2013-08-10 04:29:35", updated_at: "2013-08-10 04:29:35">
1.9.3p327 :002 > u.compound_computed_metric
=> 1.5815278998954843e-05
1.9.3p327 :003 > u.age_in_seconds
=> 126471.981447
1.9.3p327 :004 > u.computed_metric
=> 2
答案 2 :(得分:1)
让我们在此前言,这不是您正在寻找的答案......
现在,除此之外,这是我尝试的内容以及它与我在问题评论中发布的两个链接的关系。
class User < ActiveRecord::Base
# self-referential association - more on this later
belongs_to :myself, class_name: "User", foreign_key: :id
scope :w_computed_metric, ->() { select{[id, (id *2).as(computed_metric)]} }
scope :w_age_in_seconds, ->() { select{[id, (extract('epoch from now()') - extract('epoch from users.created_at')).as(age_in_seconds)]} }
scope :w_default_attributes, ->() { select{`*`} }
def self.compound_metric
scope = User.w_default_attributes.select{(b.age_in_seconds / a.computed_metric).as(compound_metric)}
scope = scope.joins{"inner join (" + User.w_computed_metric.to_sql + ") as a on a.id = users.id"}
scope = scope.joins{"inner join (" + User.w_age_in_seconds.to_sql + ") as b on b.id = users.id"}
end
sifter :sift_computed_metric do
(id * 2).as(computed_metric)
end
sifter :sift_age_in_seconds do
(extract(`epoch from now()`) - extract(`epoch from users.created_at`)).as(age_in_seconds)
end
def self.using_sifters_in_select
User.w_default_attributes.joins{myself}.select{[(myself.sift :sift_computed_metric), (myself.sift :sift_age_in_seconds)]}
end
def self.using_from
scope = User.w_default_attributes
scope = scope.select{[(age_in_seconds / computed_metric).as(compound_metric)]}
scope = scope.from{User.w_computed_metric.w_age_in_seconds}
end
end
因此,在控制台中运行User.compound_metric
将产生您要查找的结果 - 具有其他属性的User
对象:computed_metric
,age_in_seconds
和{{1 }}。不幸的是,这违反了您对可接受答案的第三个限制。哦,好吧......
我还尝试了其他一些事情(如上所示):
首先要注意的是自我指涉联想,我很自豪 - 即使它没有让我们到达我们想去的地方。
compound_metric
这段漂亮的代码允许您通过连接访问同一个对象。为什么这很重要?好吧,Squeel只允许您通过belongs_to :myself, class_name: "User", foreign_key: :id
方法访问关联,除非您传递一串SQL。这让我们可以使用Squeel的joins{}
功能 - 在这种情况下不要过滤结果,而是要包含数据库中的聚合列,让Squeel完成混叠和加入声明。您可以使用
sifter
实现这一目标的人的美丽是可连接性和合成糖 - 它非常扁平和可读。
我尝试过的最后一点是def self.using_sifters_in_select
User.w_default_attributes.joins{myself}.select{[(myself.sift :sift_computed_metric), (myself.sift :sift_age_in_seconds)]}
end
。在这个问题之前,我甚至都不知道它存在。我非常兴奋,因为我错过了包含查询源的简单内容(在这种情况下是子选择)。使用using_from
.from{}
导致TypeError:
def self.using_from
scope = User.w_default_attributes
scope = scope.select{[(age_in_seconds / computed_metric).as(compound_metric)]}
scope = scope.from{User.w_computed_metric.w_age_in_seconds}
end
(是的,我正在测试Arel和Squeel的本地副本)。我对Arel的内部工作方式不太熟悉,无需进一步努力就解决问题(很可能是Arel的一个分支)。看来,Squeel只是将TypeError: Cannot visit Arel::SelectManager
from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:28:in `rescue in visit'
from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:19:in `visit'
from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:348:in `visit_Arel_Nodes_JoinSource'
from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:21:in `visit'
from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:139:in `visit_Arel_Nodes_SelectCore'
from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:121:in `block in visit_Arel_Nodes_SelectStatement'
from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:121:in `map'
from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:121:in `visit_Arel_Nodes_SelectStatement'
from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:21:in `visit'
from /home/prg10itd/projects/arel/lib/arel/visitors/visitor.rb:5:in `accept'
from /home/prg10itd/projects/arel/lib/arel/visitors/to_sql.rb:19:in `accept'
方法传递给了Arel from{}
方法而没有做任何事情(除了Squeel的其余魔法之外)。
那么我们离开了哪里?一个有效的解决方案,但并不像我希望的那样漂亮和优雅 - 但也许其他人可以利用这个来获得更好的解决方案。
PS - 这是使用Rails v3.2.13和Arel的相应版本。 Rails v4和Arel的来源完全不同,未对此进行测试。