将generate_series与Rails ActiveRecord一起使用

时间:2019-08-23 13:59:35

标签: ruby-on-rails ruby-on-rails-5

我很难用 Rails ORM generate_series 重写纯SQL查询。

首先,解释一下我要做什么。

我的任务是从用户每月选择的日期间隔中提取发票总数。容易。

因此,如果用户选择2019年,则结果将类似于:

Month | total_sum
2       500
3       600
5       700 

问题是,结果仅包含发票中存在的月份。

要解决此问题,我想用generate_series(SQL函数)填补几个月的空白。

我想出了类似这样的SQL:

WITH 
range_values AS (
  SELECT date_trunc('month', date_start) as minval,
         date_trunc('month', date_end) as maxval
  FROM transactions),

months_range AS (
  SELECT generate_series(minval, maxval, '1 month'::interval) as month
  FROM range_values
),

monthly_sum AS (
  SELECT date_trunc('month', payment_date) AS month,
         SUM(total)   AS total
  FROM transactions
  GROUP BY month
)

SELECT date_part('month', months_range.month),
        coalesce(monthly_sum.total, 0) AS total
FROM months_range
LEFT JOIN monthly_sum ON months_range.month = monthly_sum.month;

结果是我期望的,但是我想将此方法与现有关系链接起来。

我想到了:

@relation.joins("LEFT JOIN generate_series(TIMESTAMP '1-1-2018', TIMESTAMP '1-1-2021', interval '1 month') AS series ON transactions.payment_date = series")
.group("series")
.select("series, sum(transactions.total)"`)

跟踪生成的查询:

SELECT series, sum(transactions.total) 
FROM transactions 
LEFT JOIN generate_series(TIMESTAMP '1-1-2018', TIMESTAMP '1-1-2021', interval '1 month') AS series ON transactions.payment_date = series 
GROUP BY series 

rails的结果是:

#<ActiveRecord::Relation [#<Transaction id: nil>]>

注意:我期望的是类似[{“ series” =>'x',“ total” =>'y'},{#...},#的数组。 ..]不是Transaction个对象

如果我在 pgadmin 中运行此生成的SQL,结果为:

series | sum
null     5881

使用:

  • Rails 5.2
  • Postgresql 9.1

编辑

我现在得到了什么,但是还没有得到结果:

  def group_total_by_months
   @relation.joins("RIGHT JOIN generate_series(TIMESTAMP '1-1-2018', TIMESTAMP '1-1-2020', interval '1 month') AS series
                     ON date_trunc('month', transactions.payment_date) = series")
            .group("series")
            .select("series, sum(transactions.total) AS total")
  end

结果:

#<ActiveRecord::AssociationRelation [#<Transaction id: nil, total: 0.61173e3>, #<Transaction id: nil, total: 0.364446e4>, #<Transaction id: nil, total: 0.1625e4>]>

结果是正确的3个月,但缺少日期序列。 to_sql:

   SELECT series AS payment_date, sum(transactions.total) AS total FROM 
 \"transactions\"
 RIGHT JOIN generate_series(TIMESTAMP '1-1-2018', TIMESTAMP '1-1-2020', 
          interval '1 month') AS series ON date_trunc('month', 
          transactions.payment_date) = series 
WHERE \"transactions\".\"account_id\" = 1 
GROUP BY series

我想要实现的目标:

WITH filteret_transactions AS (
    SELECT * FROM transactions WHERE transactions.account_id = 1
)

 SELECT series AS payment_date, sum(filteret_transactions.total) AS total 
    FROM  filteret_transactions
  RIGHT JOIN generate_series(TIMESTAMP '1-1-2018', TIMESTAMP '1-1-2020', interval '1 month') AS series
  ON date_trunc('month', filteret_transactions.payment_date) = series GROUP BY series

1 个答案:

答案 0 :(得分:1)

我认为以下内容应该对您有用

def sums_by_month_range(date1,date2,relates_to, date_column: :payment_date, sum_column: :total)
  relation_table = Arel::Table.new(:relation)
  relation = Arel::Nodes::As.new(relation_table, relates_to.arel) 
  month_range_table = Arel::Table.new(:month_range)
  month_range = Arel::Nodes::As.new(month_range_table,
    Arel::SelectManager.new.project(
      Arel::Nodes::NamedFunction.new('date_part',[Arel.sql("'month'"),Arel::Attribute.new(Arel::Table.new(:series),'month')]).as('month')
    ).distinct.from(
     Arel::Nodes::NamedFunction.new('generate_series',[Arel.sql("date '#{date1.strftime('%Y-%m-%d')}'"),Arel.sql("date '#{date2.strftime('%Y-%m-%d')}'"),Arel.sql("'1 month'::interval")]).as('series(month)').to_sql
    )
  )
  ActiveRecord::Base.connection.exec_query(
    month_range_table.project(
      month_range_table[:month],
      relation_table[sum_column].sum.as('total')
    ).with(month_range, relation).join(relation_table, Arel::Nodes::OuterJoin).on(
      Arel::Nodes::NamedFunction.new('date_trunc',[Arel.sql("'month'"),relation_table[date_column]]).eq(month_range_table[:month])
    ).group(month_range_table[:month]).to_sql
  ).to_hash
end 

要重新创建第二个示例,可以将其称为:

sums_by_month(Date.new(2018,1,1),Date.new(2021,1,1), @relation)

这将返回Array中的Hash[{'month' => x, 'total' => y}],您可以根据需要进行修改。查询将类似于

WITH month_range AS  (
  SELECT DISTINCT
    date_part('month', [series].[month]) AS month 
  FROM 
    generate_series(date '2018-01-01', date '2019-01-01', '1 month'::interval) AS series(month)
), relation AS ( 
  [WHATEVER AR QUERY OBJECT YOU PASS IN]
)
SELECT 
   month_range.month,
   SUM(relation.total) AS total
FROM 
   month_range 
   LEFT OUTER JOIN relation ON date_trunc('month', relation.payment_date) = month_range.month
GROUP BY 
   month_range.month

TL; DR 这将重现您的确切查询(帖子中的示例1)

transaction_table = Transaction.arel_table
range_values_table = Arel::Table.new(:range_values)
range_values = Arel::Nodes::As.new(range_values_table,
  transaction_table.project(
    Arel::Nodes::NamedFunction.new('date_trunc',[Arel.sql("'month'"),transaction_table[:date_start]]).as('minval'),
    Arel::Nodes::NamedFunction.new('date_trunc',[Arel.sql("'month'"),transaction_table[:date_end]]).as('maxval')
  )
)

month_range_table = Arel::Table.new(:months_range)
month_range = Arel::Nodes::As.new(month_range_table,
  range_values_table.project(
    Arel::Nodes::NamedFunction.new('generate_series',[range_values_table[:minval],range_values_table[:maxval],Arel.sql("'1 month'::interval")]).as('month')
  )
)

monthly_sum_table = Arel::Table.new(:monthly_sum)
monthly_sum = Arel::Nodes::As.new(monthly_sum_table,
  transaction_table.project(
    Arel::Nodes::NamedFunction.new('date_trunc',[Arel.sql("'month'"),transaction_table[:payment_date]]).as('month'),
    transaction_table[:total].sum.as('total')
  ).group(Arel::Nodes::NamedFunction.new('date_trunc',[Arel.sql("'month'"),transaction_table[:payment_date]])))

query = month_range_table.project(
  Arel::Nodes::NamedFunction.new('date_part',[Arel.sql("'month'"),month_range_table[:month]]).as('month'),
  Arel::Nodes::NamedFunction.new('coalesce',[monthly_sum_table[:total],Arel.sql("0")]).as('total')
  ).with( 
    range_values,
    month_range,
    monthly_sum
  ).join(monthly_sum_table, Arel::Nodes::OuterJoin).on(monthly_sum_table[:month].eq(month_range_table[:month]))

ActiveRecord::Base.connection.exec_query(query.to_sql).to_hash

生成的SQL

WITH range_values AS (
    SELECT 
        date_trunc('month', transactions.date_start) AS minval, 
        date_trunc('month', transactions.date_end) AS maxval 
    FROM 
        transactions), 
months_range AS (
    SELECT 
        generate_series(range_values.minval, range_values.maxval, '1 month'::interval) AS month 
    FROM 
        range_values), 
monthly_sum AS (
    SELECT 
        date_trunc('month', transactions.payment_date) AS month, 
        SUM(transactions.total) AS total 
    FROM 
        transactions 
    GROUP BY 
        date_trunc('month', transactions.payment_date)) 

SELECT 
    date_part('month', months_range.month) AS month, 
    coalesce(monthly_sum.total, 0) AS total 
FROM 
    months_range 
    LEFT OUTER JOIN monthly_sum ON monthly_sum.month = months_range.month