如何将 table 与 tree-structure 聚合为单个嵌套 JSON object?
How to aggregate a table with tree-structure to a single nested JSON object?
我在具有 self-referencing 树结构的 Postgres 11.4 数据库中有一个 table:
+------------+
| account |
+------------+
| id |
| code |
| type |
| parentId | -- references account.id
+------------+
每个child可以有另一个child,嵌套层数没有限制
我想从中生成一个 JSON object,嵌套所有 children(resursivly)。
是否可以通过单个查询来解决这个问题?
或任何其他使用 typeORM 和 table?
的解决方案
否则我将不得不在服务器端手动绑定数据。
我试过这个查询:
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$;
准确调用(returns 想要的结果):
SELECT jsonb_pretty(f_build_jsonb_tree());
db<>fiddle here - 带有扩展测试用例
我选择了键名 children
而不是 child
,因为可以嵌套多个。
jsonb_pretty()
美化显示是可选的。
这是假设参照完整性;应该用 FK 约束来实现。
对于您的特定情况,解决方案可能更简单,利用 code
列 - 如果它展示(未公开的)有用的属性。就像我们可能在没有 rCTE 的情况下导出嵌套级别并添加临时 table t
。但我的目标是仅基于 ID 引用的通用解决方案。
函数中发生了很多事情。我添加了内联评论。基本上,它是这样做的:
- 创建一个临时 table 添加嵌套级别 (
lvl
)
- 如果没有发现嵌套,return简单结果
- 如果找到嵌套,请从顶部嵌套级别向下折叠到
jsonb
。
将所有中间结果写入第二个临时 table j
.
- 一旦我们到达第二个嵌套级别,return 完整结果。
该函数仅将 _type
作为给定类型的 return 参数。否则,将处理整个 table。
旁白:尽可能避免在 Postgres 中使用大小写混合的标识符,例如 "parentId"
。参见:
- Are PostgreSQL column names case-sensitive?
使用递归函数:
我在具有 self-referencing 树结构的 Postgres 11.4 数据库中有一个 table:
+------------+
| account |
+------------+
| id |
| code |
| type |
| parentId | -- references account.id
+------------+
每个child可以有另一个child,嵌套层数没有限制
我想从中生成一个 JSON object,嵌套所有 children(resursivly)。
是否可以通过单个查询来解决这个问题?
或任何其他使用 typeORM 和 table?
的解决方案
否则我将不得不在服务器端手动绑定数据。
我试过这个查询:
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$;
准确调用(returns 想要的结果):
SELECT jsonb_pretty(f_build_jsonb_tree());
db<>fiddle here - 带有扩展测试用例
我选择了键名 children
而不是 child
,因为可以嵌套多个。
jsonb_pretty()
美化显示是可选的。
这是假设参照完整性;应该用 FK 约束来实现。
对于您的特定情况,解决方案可能更简单,利用 code
列 - 如果它展示(未公开的)有用的属性。就像我们可能在没有 rCTE 的情况下导出嵌套级别并添加临时 table t
。但我的目标是仅基于 ID 引用的通用解决方案。
函数中发生了很多事情。我添加了内联评论。基本上,它是这样做的:
- 创建一个临时 table 添加嵌套级别 (
lvl
) - 如果没有发现嵌套,return简单结果
- 如果找到嵌套,请从顶部嵌套级别向下折叠到
jsonb
。
将所有中间结果写入第二个临时 tablej
. - 一旦我们到达第二个嵌套级别,return 完整结果。
该函数仅将 _type
作为给定类型的 return 参数。否则,将处理整个 table。
旁白:尽可能避免在 Postgres 中使用大小写混合的标识符,例如 "parentId"
。参见:
- Are PostgreSQL column names case-sensitive?
使用递归函数: