我在 Postgres 11.4 数据库中有一个具有自引用树结构的表:
+------------+
| account |
+------------+
| id |
| code |
| type |
| parentId | -- references account.id
+------------+
每个 child 可以有另一个 child ,嵌套级别没有限制。
我想从中生成一个 JSON 对象,嵌套所有子对象(resurivly)。
是否可以通过单个查询来解决这个问题?
或者任何其他使用 typeORM 和一张 table 的解决方案?
否则我将不得不在服务器端手动绑定(bind)数据。
我试过这个查询:
SELECT account.type, json_agg(account) as accounts
FROM account
-- LEFT JOIN "account" "child" ON "child"."parentId"="account"."id" -- tried to make one column child
GROUP BY account.type
结果:
[
...
{
"type": "type03",
"accounts": [
{
"id": 28,
"code": "acc03.001",
"type": "type03",
"parentId": null
},
{
"id": 29,
"code": "acc03.001.001",
"type": "type03",
"parentId": 28
},
{
"id": 30,
"code": "acc03.001.002",
"type": "type03",
"parentId": 28
}
]
}
...
]
我希望这样:
[
...
{
"type": "type03",
"accounts": [
{
"id": 28,
"code": "acc03.001",
"type": "type03",
"parentId": null,
"child": [
{
"id": 29,
"code": "acc03.001.001",
"type": "type03",
"parentId": 28
},
{
"id": 30,
"code": "acc03.001.002",
"type": "type03",
"parentId": 28
}
]
}
]
}
...
]
最佳答案
这是棘手 .
这是一个递归问题,但标准 recursive CTEs没有能力处理它,因为我们需要在每个级别上进行聚合,而 CTE 不允许在递归项中进行聚合。
我用 PL/pgSQL 函数解决了这个问题:
CREATE OR REPLACE FUNCTION f_build_jsonb_tree(_type text = NULL)
RETURNS jsonb
LANGUAGE plpgsql AS
$func$
DECLARE
_nest_lvl int;
BEGIN
-- add level of nesting recursively
CREATE TEMP TABLE t ON COMMIT DROP AS
WITH RECURSIVE t AS (
SELECT *, 1 AS lvl
FROM account
WHERE "parentId" IS NULL
AND (type = _type OR _type IS NULL) -- default: whole table
UNION ALL
SELECT a.*, lvl + 1
FROM t
JOIN account a ON a."parentId" = t.id
)
TABLE t;
-- optional idx for big tables with many levels of nesting
-- CREATE INDEX ON t (lvl, id);
_nest_lvl := (SELECT max(lvl) FROM t);
-- no nesting found, return simple result
IF _nest_lvl = 1 THEN
RETURN ( -- exits functions
SELECT jsonb_agg(sub) -- AS result
FROM (
SELECT type
, jsonb_agg(sub) AS accounts
FROM (
SELECT id, code, type, "parentId", NULL AS children
FROM t
ORDER BY type, id
) sub
GROUP BY 1
) sub
);
END IF;
-- start collapsing with leaves at highest level
CREATE TEMP TABLE j ON COMMIT DROP AS
SELECT "parentId" AS id
, jsonb_agg (sub) AS children
FROM (
SELECT id, code, type, "parentId" -- type redundant?
FROM t
WHERE lvl = _nest_lvl
ORDER BY id
) sub
GROUP BY "parentId";
-- optional idx for big tables with many levels of nesting
-- CREATE INDEX ON j (id);
-- iterate all the way down to lvl 2
-- write to same table; ID is enough to identify
WHILE _nest_lvl > 2
LOOP
_nest_lvl := _nest_lvl - 1;
INSERT INTO j(id, children)
SELECT "parentId" -- AS id
, jsonb_agg(sub) -- AS children
FROM (
SELECT id, t.code, t.type, "parentId", j.children -- type redundant?
FROM t
LEFT JOIN j USING (id) -- may or may not have children
WHERE t.lvl = _nest_lvl
ORDER BY id
) sub
GROUP BY "parentId";
END LOOP;
-- nesting found, return nested result
RETURN ( -- exits functions
SELECT jsonb_agg(sub) -- AS result
FROM (
SELECT type
, jsonb_agg (sub) AS accounts
FROM (
SELECT id, code, type, "parentId", j.children
FROM t
LEFT JOIN j USING (id)
WHERE t.lvl = 1
ORDER BY type, id
) sub
GROUP BY 1
) sub
);
END
$func$;
调用(准确返回所需结果):SELECT jsonb_pretty(f_build_jsonb_tree());
db<> fiddle here - 带有扩展测试用例我选择了键名
children
而不是 child
,因为可以嵌套多个。jsonb_pretty()
美化显示是可选的。这是假设参照完整性;应该使用 FK 约束来实现。
对于您的特定情况,使用
code
的解决方案可能会更简单。列 - 如果它表现出(未公开)有用的特性。就像我们可能会在没有 rCTE 的情况下导出嵌套级别并添加临时表 t
.但我的目标是 一般解决方案仅基于 ID 引用。函数中发生了很多事情。我添加了内联注释。基本上,它这样做:
lvl
) 的临时表 jsonb
从顶层嵌套层往下。将所有中间结果写入第二个临时表
j
. 该函数采用
_type
作为参数只返回给定的类型。否则,将处理整个表。另外:避免使用大小写混合的标识符,如
"parentId"
如果可能的话,在 Postgres 中。看:稍后使用 进行相关回答递归函数 :
关于sql - 如何将具有树结构的表聚合到单个嵌套 JSON 对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62280978/