Arel在聚合上导致无限循环

时间:2010-10-13 19:55:44

标签: sql ruby-on-rails ruby ruby-on-rails-3 arel

我在使用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]))

1 个答案:

答案 0 :(得分: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);

最终,为了获得您所追求的数据,这是您使用的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

你必须先在用户之间进行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

经过多次尝试后,我发现这个问题代码与我们的代码最接近:

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'))

基本上,我在第一个构造中加入用户并使用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`

运行时,产生此输出:

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

非常接近,但并不完全。首先,缺少COALESCE会给我们NULL值而不是零 - 我不确定如何从arel中实现COALESCE函数调用。

更重要的是,将LEFT OUTER JOIN组合成一个没有内部子选择的语句会导致sum_paid总数在示例2,3和4中成倍增加 - 任何时候多于一个收费或付款至少一个

根据一些在线阅读材料,我曾希望稍微改变这个问题可以解决问题:

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

但是当我在第二个arel构造中使用user_charges []时,我得到了 SelectManager#[] 的未定义方法错误。这可能是一个错误,或者可能是对的 - 我实在说不出来。

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