Arel在聚合上导致无限循环

fin*_*vin 7 ruby sql ruby-on-rails arel ruby-on-rails-3

我在使用Arel在同一查询中聚合2列时遇到问题.当我运行它时,整个服务器在rails dev-server崩溃之前冻结了一分钟.我怀疑是无限循环:).

也许我误解了Arel的整个概念,如果有人能看一下,我将不胜感激.

此查询的预期结果如下所示:[{:user_id => 1,:sum_account_charges => 300,:sum_paid_debts => 1000},...]

a_account_charges = Table(:account_charges)
a_paid_debts = Table(:paid_debts)
a_participants = Table(:expense_accounts_users)

account_charge_sum = a_account_charges
  .where(a_account_charges[:expense_account_id].eq(id))
  .group(a_account_charges[:user_id])
  .project(a_account_charges[:user_id], a_account_charges[:cost].sum)

paid_debts_sum = a_paid_debts
 .where(a_paid_debts[:expense_account_id].eq(id))
 .group(a_paid_debts[:from_user_id])
 .project(a_paid_debts[:from_user_id], a_paid_debts[:cost].sum)

charges = a_participants
 .where(a_participants[:expense_account_id].eq(id))
 .join(account_charge_sum)
 .on(a_participants[:user_id].eq(account_charge_sum[:user_id]))
 .join(paid_debts_sum)
 .on(a_participants[:user_id].eq(paid_debts_sum[:from_user_id]))
Run Code Online (Sandbox Code Playgroud)

Yar*_*boy 4

我是 arel 的新手,但是经过几天的研究和真正的挖掘后,我认为这是不可能的。以上是我所做的工作的概述,如果有人有任何其他见解,将受到欢迎。

首先,这些脚本将创建测试表并用测试数据填充它们。我设置了9个expense_account_users,每个都有一组不同的费用/paid_debts,如下:1个费用/1个付款,2个费用/2个付款,2个费用/1个付款,2个费用/0个付款,1个费用/2个付款, 0 笔费用/2 笔付款、1 笔费用/0 笔付款、0 笔费用/1 笔付款、0 笔费用、0 笔付款。

CREATE TABLE IF NOT EXISTS `expense_accounts_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `expense_account_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;

INSERT INTO `expense_accounts_users` (`id`, `expense_account_id`) VALUES (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1);

CREATE TABLE IF NOT EXISTS `account_charges` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `expense_account_id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `cost` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;

INSERT INTO `account_charges` (`id`, `expense_account_id`, `user_id`, `cost`) VALUES (1, 1, 1, 1), (2, 1, 2, 1), (3, 1, 2, 2), (4, 1, 3, 1), (5, 1, 3, 2), (6, 1, 4, 1), (7, 1, 5, 1), (8, 1, 5, 2), (9, 1, 7, 1);

CREATE TABLE IF NOT EXISTS `paid_debts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `expense_account_id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `cost` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10 ;

INSERT INTO `paid_debts` (`id`, `expense_account_id`, `user_id`, `cost`) VALUES (1, 1, 1, 1), (2, 1, 2, 1), (3, 1, 2, 2), (4, 1, 3, 1), (5, 1, 4, 1), (6, 1, 4, 2), (7, 1, 6, 1), (8, 1, 6, 2), (9, 1, 8, 1);
Run Code Online (Sandbox Code Playgroud)

最终,为了一次性获得您想要的数据,您可以使用以下 SQL 语句:

SELECT user_charges.user_id,
  user_charges.sum_cost,
  COALESCE(SUM(paid_debts.cost), 0) AS 'sum_paid'
FROM (
  SELECT expense_accounts_users.id AS 'user_id',
  COALESCE(sum(account_charges.cost), 0) AS 'sum_cost'
  FROM expense_accounts_users
  LEFT OUTER JOIN account_charges on expense_accounts_users.id = account_charges.user_id
  GROUP BY expense_accounts_users.id)
AS user_charges
LEFT OUTER JOIN paid_debts ON user_charges.user_id = paid_debts.user_id
GROUP BY user_charges.user_id
Run Code Online (Sandbox Code Playgroud)

您必须首先在用户和费用之间执行 LEFT OUTER JOIN,以便为每个用户获取一行,然后必须将结果LEFT OUTER JOIN与债务相乘,以避免将结果与同一构造内的两个联接相乘。

(注意使用COALESCE将 NULL 值从 LEFT OUTER JOIN 转换为零 - 也许是一个方便的项目)

这个语句的结果是这样的:

user_id   sum_cost  sum_paid
1         1         1
2         3         3
3         3         1
4         1         3
5         3         0
6         0         3
7         1         0
8         0         1
9         0         0
Run Code Online (Sandbox Code Playgroud)

经过多次尝试,我发现这个 arel 代码最接近我们想要的:

c = Arel::Table.new(:account_charges)
d = Arel::Table.new(:paid_debts)
p = Arel::Table.new(:expense_accounts_users)
user_charges = p
 .where(p[:expense_account_id].eq(1))
 .join(c, Arel::Nodes::OuterJoin)
 .on(p[:id].eq(c[:user_id]))
 .project(p[:id], c[:cost].sum.as('sum_cost'))
 .group(p[:id])
charges = user_charges
 .join(d, Arel::Nodes::OuterJoin)
 .on(p[:id].eq(d[:user_id]))
 .project(d[:cost].sum.as('sum_paid'))
Run Code Online (Sandbox Code Playgroud)

本质上,我使用 LEFT OUTER JOIN 将用户加入到第一个构造中的费用中,然后尝试获取结果并将 LEFT OUTER JOIN 返回到债务。此 arel 代码生成以下 SQL 语句:

SELECT `expense_accounts_users`.`id`,
  SUM(`account_charges`.`cost`) AS sum_cost,
  SUM(`paid_debts`.`cost`) AS sum_paid
FROM `expense_accounts_users`
LEFT OUTER JOIN `account_charges` ON `expense_accounts_users`.`id` = `account_charges`.`user_id`
LEFT OUTER JOIN `paid_debts` ON `expense_accounts_users`.`id` = `paid_debts`.`user_id`
WHERE `expense_accounts_users`.`expense_account_id` = 1
GROUP BY `expense_accounts_users`.`id`
Run Code Online (Sandbox Code Playgroud)

运行时会产生以下输出:

id  sum_cost  sum_paid
1   1         1
2   6         6
3   3         2
4   2         3
5   3         NULL
6   NULL      3
7   1         NULL
8   NULL      1
9   NULL      NULL
Run Code Online (Sandbox Code Playgroud)

非常接近,但不完全是。首先,缺少 COALESCE 给我们提供了 NULL 值而不是零 - 我不确定如何从 arel 内部调用 COALESCE 函数。

但更重要的是,在没有内部子选择的情况下将 LEFT OUTER JOIN 合并到一个语句中会导致示例 2、3 和 4 中的 sum_paid 总数相乘 - 任何时候,无论是收费还是付款,且至少有一项,另一个。

根据网上的一些阅读,我希望稍微改变一下arel就能解决问题:

charges = user_charges
 .join(d, Arel::Nodes::OuterJoin)
 .on(user_charges[:id].eq(d[:user_id]))
 .project(d[:cost].sum.as('sum_paid'))
Run Code Online (Sandbox Code Playgroud)

但是每当我在第二个 arel 构造中使用 user_charges[] 时,我都会收到SelectManager#[]的未定义方法错误。这可能是一个错误,也可能是对的——我真的不能说。

我只是没有看到 arel 有办法利用第一个构造中的 SQL 作为第二个构造中的可查询对象,并具有必要的子查询别名,这是在一个 SQL 语句中实现这一点所需的。

  • 您可以通过创建命名函数来添加 COALESCE 函数: Arel::Nodes::NamedFunction.new(:COALESCE, [t[:your_column],0) 将生成正确的 SQL 输出。 (2认同)