Арель вызывает бесконечный цикл по агрегации - PullRequest
7 голосов
/ 13 октября 2010

У меня проблемы с использованием Arel для объединения 2 столбцов в одном запросе. Когда я запускаю это, весь сервер останавливается на минуту, прежде чем rails dev-server выходит из строя. Я подозреваю, что бесконечный цикл :).

Может быть, я неправильно понял всю концепцию Арела, и я был бы признателен, если бы кто-нибудь взглянул на это.

Ожидаемый результат этого запроса примерно такой: [{: 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 Ответ

4 голосов
/ 10 июня 2011

Я новичок в арле, но после нескольких дней стука и действительно копания я не думаю, что это можно сделать. Вот краткое изложение того, что я сделал, если у кого-то есть дополнительная информация, это будет приветствоваться.

Во-первых, эти сценарии будут создавать тестовые таблицы и заполнять их тестовыми данными. Я настроил 9 cost_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

Сначала необходимо выполнить СЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ между пользователями и начислениями так, чтобы вы получили строку для каждого пользователя, затем вам нужно ВЛЕВОЕ ВНЕШНЕЕ СОЕДИНЕНИЕ результат к долгам, чтобы избежать умножения результатов. с двумя соединениями внутри одной конструкции.

(обратите внимание на использование 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

После многих попыток я обнаружил, что этот код 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'))

По сути, я присоединяю пользователей к начислениям в первой конструкции с помощью 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 вместо нулей - я не уверен, как осуществить вызов функции COALESCE изнутри arel.

Что еще более важно, объединение ЛЕВЫХ ВНЕШНИХ СОЕДИНЕНИЙ в один оператор без внутреннего подвыбора приводит к тому, что итоговые суммы 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'))

Но каждый раз, когда я использовал user_charges [] во второй конструкции arel, я получал неопределенную ошибку метода для SelectManager # [] . Это может быть ошибкой, или может быть правильным - я действительно не могу сказать.

Я просто не вижу, чтобы у arel был способ использовать SQL из первой конструкции в качестве запрашиваемого объекта во второй конструкции с необходимым псевдонимом подзапроса, что необходимо для того, чтобы это произошло в одном операторе SQL.

...