#ruby-on-rails #activerecord #arel
#ruby-на-рельсах #активная запись #arel
Вопрос:
У меня есть простая таблица для представления иерархии организаций:
CREATE TABLE public.organizations (
id integer NOT NULL,
name character varying(255) NOT NULL,
parent_id integer,
deleted_at timestamp without time zone
)
Цель состоит в том, чтобы, учитывая некоторый запрос для некоторых организаций, расширить эти результаты, включив в них всех потомков. В Postgres это можно сделать с помощью рекурсивного CTE, например:
WITH RECURSIVE "all_orgs" AS (
SELECT "organizations".*
FROM "organizations"
/* some filter, maybe some joins here */
UNION
SELECT "organizations".*
FROM "organizations"
INNER JOIN "all_orgs" ON "all_orgs"."id" = "organizations"."parent_id"
)
SELECT "all_orgs".* FROM "all_orgs"
Я хотел бы запрограммировать повторно используемый способ включения всех потомков для любого произвольного начального набора организаций. Поэтому, естественно, я попытался реализовать область применения:
class Organization << ApplicationRecord
scope :recursive_child_orgs, ->(root_orgs) do
all_orgs = Arel::Table.new(:all_orgs)
join_constraint = Arel::Nodes::On.new(all_orgs[:id].eq(arel_table[:parent_id]))
join_node = Arel::Nodes::InnerJoin.new(all_orgs, join_constraint)
child_orgs = joins(join_node)
union = root_orgs.arel.union(child_orgs.arel)
recursive_cte = Arel::SelectManager.new(all_orgs).tap do |sm|
sm.with(:recursive, Arel::Nodes::As.new(all_orgs, union))
sm.project(all_orgs[Arel::star])
end
from(recursive_cte.as(table_name))
end
end
Это было бы использовано примерно так Organization.recursive_child_orgs(Organization.where(id: 3))
(на практике внутренняя часть этого выражения была бы чем-то менее тривиальным).
Это мучительно близко, генерируя запрос:
SELECT "organizations".* FROM (
WITH RECURSIVE "all_orgs" AS (
SELECT "organizations".*
FROM "organizations"
WHERE "organizations"."deleted_at" IS NULL
AND "organizations"."id" = /* note: missing value! */
UNION
SELECT "organizations".*
FROM "organizations"
INNER JOIN "all_orgs" ON "all_orgs"."id" = "organizations"."parent_id"
WHERE "organizations"."deleted_at" IS NULL
)
SELECT "all_orgs".* FROM "all_orgs"
) organizations
WHERE "organizations"."deleted_at" IS NULL;
Область по умолчанию для Organization
была включена, приятно! И весь этот беспорядок CTE объединен в виде объекта с именем organizations
, что означает, что ActiveRecord выглядит как таблица организаций, поэтому добавление сортировки и еще много чего после рекурсивного бита должно сработать.
Но, к сожалению, связанный параметр, который был введен с Organization.where(id: 3)
помощью, не выдерживает перевода, и в результате генерируется синтаксически недопустимый оператор. Как это можно исправить?