你会如何使用Squeel进行这个Rails子查询?

时间:2013-08-01 12:38:43

标签: ruby-on-rails activerecord arel squeel

我想使用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_aboutage_in_secondscompound_computed_metric
  • 查询不应该只通过在多个地方打印出一个字符串来复制任何逻辑 - 我想避免两次做同样的事情
  • [更新]查询应该在数据库中都可以执行,以便在返回到Rails之前可以在数据库中对可能包含数百万条记录的结果集进行排序和切片
  • [已更新]解决方案适用于Postgres DB

我想要的解决方案类型

下面的解决方案不起作用,但它显示了我希望实现的优雅类型

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课程并在控制台中使用它。

修改

  1. 我正在使用的数据库是Postgres。
  2. 我不确定我是否100%明确查询应该全部在DB中执行。它不能是一个混合的答案,一些逻辑基本上是在Rails中完成的。这很重要,因为我希望能够使用计算列对数百万条记录进行排序和切片。

3 个答案:

答案 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_metricage_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的来源完全不同,未对此进行测试。