关于json:PostgreSQL 9.2 row_to_json()与嵌套连接

PostgreSQL 9.2 row_to_json() with nested joins

我正在尝试使用PostgreSQL 9.2中添加的row_to_json()函数将查询结果映射到JSON。

我无法找出将连接行表示为嵌套对象的最佳方法(1:1关系)

这是我尝试过的(设置代码:表格,示例数据,然后是查询):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- some test tables to start out with:
CREATE TABLE role_duties (
    id serial PRIMARY KEY,
    name VARCHAR
);

CREATE TABLE user_roles (
    id serial PRIMARY KEY,
    name VARCHAR,
    description VARCHAR,
    duty_id INT, FOREIGN KEY (duty_id) REFERENCES role_duties(id)
);

CREATE TABLE users (
    id serial PRIMARY KEY,
    name VARCHAR,
    email VARCHAR,
    user_role_id INT, FOREIGN KEY (user_role_id) REFERENCES user_roles(id)
);

DO $$
DECLARE duty_id INT;
DECLARE role_id INT;
BEGIN
INSERT INTO role_duties (name) VALUES ('Script Execution') returning id INTO duty_id;
INSERT INTO user_roles (name, description, duty_id) VALUES ('admin', 'Administrative duties in the system', duty_id) returning id INTO role_id;
INSERT INTO users (name, email, user_role_id) VALUES ('Dan', '[email protected]', role_id);
END$$;

查询本身:

1
2
3
4
5
6
7
SELECT row_to_json(ROW)
FROM (
    SELECT u.*, ROW(ur.*::user_roles, ROW(d.*::role_duties)) AS user_role
    FROM users u
    INNER JOIN user_roles ur ON ur.id = u.user_role_id
    INNER JOIN role_duties d ON d.id = ur.duty_id
) ROW;

我发现如果我使用ROW(),我可以将结果字段分成一个子对象,但它似乎仅限于一个级别。我不能插入更多AS XXX语句,因为我认为在这种情况下我应该需要。

我获得了列名,因为我转换为适当的记录类型,例如使用::user_roles,就该表的结果而言。

这是查询返回的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "id":1,
  "name":"Dan",
  "email":"[email protected]",
  "user_role_id":1,
  "user_role":{
     "f1":{
        "id":1,
        "name":"admin",
        "description":"Administrative duties in the system",
        "duty_id":1
      },
     "f2":{
        "f1":{
           "id":1,
           "name":"Script Execution"
         }
      }
   }
}

我想要做的是为连接生成JSON(再次1:1很好),我可以添加连接,并将它们表示为它们加入的父对象的子对象,即如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "id":1,
  "name":"Dan",
  "email":"[email protected]",
  "user_role_id":1,
  "user_role":{
        "id":1,
        "name":"admin",
        "description":"Administrative duties in the system",
        "duty_id":1
        "duty":{
           "id":1,
           "name":"Script Execution"
         }
      }
   }
}

任何帮助表示赞赏。谢谢阅读。


更新:在PostgreSQL 9.4中,通过引入to_jsonjson_build_objectjson_objectjson_build_array可以大大改善,但由于需要明确命名所有字段,因此它很冗长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT
        json_build_object(
                'id', u.id,
                'name', u.name,
                'email', u.email,
                'user_role_id', u.user_role_id,
                'user_role', json_build_object(
                        'id', ur.id,
                        'name', ur.name,
                        'description', ur.description,
                        'duty_id', ur.duty_id,
                        'duty', json_build_object(
                                'id', d.id,
                                'name', d.name
                        )
                )
    )
FROM users u
INNER JOIN user_roles ur ON ur.id = u.user_role_id
INNER JOIN role_duties d ON d.id = ur.duty_id;

对于旧版本,请继续阅读。

它不仅限于一排,它只是有点痛苦。您不能使用AS对复合行类型进行别名,因此您需要使用别名子查询表达式或CTE来实现此效果:

1
2
3
4
5
6
7
8
9
10
SELECT row_to_json(ROW)
FROM (
    SELECT u.*, urd AS user_role
    FROM users u
    INNER JOIN (
        SELECT ur.*, d
        FROM user_roles ur
        INNER JOIN role_duties d ON d.id = ur.duty_id
    ) urd(id,name,description,duty_id,duty) ON urd.id = u.user_role_id
) ROW;

通过http://jsonprettyprint.com/制作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
 "id": 1,
 "name":"Dan",
 "email":"[email protected]",
 "user_role_id": 1,
 "user_role": {
   "id": 1,
   "name":"admin",
   "description":"Administrative duties in the system",
   "duty_id": 1,
   "duty": {
     "id": 1,
     "name":"Script Execution"
    }
  }
}

当你有1:很多关系时,你会想要使用array_to_json(array_agg(...)),顺便说一句。

理想情况下,上述查询应该写成:

1
2
3
4
5
6
SELECT row_to_json(
    ROW(u.*, ROW(ur.*, d AS duty) AS user_role)
)
FROM users u
INNER JOIN user_roles ur ON ur.id = u.user_role_id
INNER JOIN role_duties d ON d.id = ur.duty_id;

...但是PostgreSQL的ROW构造函数不接受AS列别名。可悲的是。

值得庆幸的是,他们优化了相同的。比较计划:

  • 嵌套的子查询版本; VS
  • 后者嵌套ROW构造函数版本,删除了别名,以便执行

因为CTE是优化围栏,所以将嵌套子查询版本重新构造为使用链式CTE(WITH表达式)可能效果不佳,并且不会产生相同的计划。在这种情况下,你会遇到丑陋的嵌套子查询,直到我们对row_to_json进行一些改进或者更直接地覆盖ROW构造函数中的列名。

无论如何,一般来说,原则是你想用列a, b, c创建一个json对象,你希望你可以编写非法语法:

1
ROW(a, b, c) AS outername(name1, name2, name3)

您可以改为使用返回行类型值的标量子查询:

1
(SELECT x FROM (SELECT a AS name1, b AS name2, c AS name3) x) AS outername

要么:

1
(SELECT x FROM (SELECT a, b, c) AS x(name1, name2, name3)) AS outername

另外,请记住,您可以在没有其他引用的情况下撰写json值,例如如果将json_agg的输出放在row_to_json中,则内部json_agg结果将不会作为字符串引用,它将直接作为json合并。

例如在任意的例子中:

1
2
3
4
5
6
7
8
9
SELECT row_to_json(
        (SELECT x FROM (SELECT
                1 AS k1,
                2 AS k2,
                (SELECT json_agg( (SELECT x FROM (SELECT 1 AS a, 2 AS b) x) )
                 FROM generate_series(1,2) ) AS k3
        ) x),
        TRUE
);

输出是:

1
2
3
4
{"k1":1,
"k2":2,
"k3":[{"a":1,"b":2},
 {"a":1,"b":2}]}

请注意,json_agg产品[{"a":1,"b":2}, {"a":1,"b":2}]尚未再次转义,因为text

这意味着您可以组合json操作来构造行,您不必总是创建非常复杂的PostgreSQL复合类型,然后在输出上调用row_to_json


我对长期可维护性的建议是使用VIEW构建查询的粗略版本,然后使用如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE OR REPLACE FUNCTION fnc_query_prominence_users( )
RETURNS json AS $$
DECLARE
    d_result            json;
BEGIN
    SELECT      ARRAY_TO_JSON(
                    ARRAY_AGG(
                        ROW_TO_JSON(
                            CAST(ROW(users.*) AS prominence.users)
                        )
                    )
                )
        INTO    d_result
        FROM    prominence.users;
    RETURN d_result;
END; $$
LANGUAGE plpgsql
SECURITY INVOKER;

在这种情况下,对象prominence.users是一个视图。由于我选择了用户。*,如果我需要更新视图以在用户记录中包含更多字段,我将不必更新此功能。