【问题标题】:Using row_to_json() with nested joins使用带有嵌套连接的 row_to_json()
【发布时间】:2012-10-24 23:53:52
【问题描述】:

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

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

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

-- 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', 'someemail@gmail.com', role_id);
END$$;

查询本身:

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,在该表的结果的情况下。

这是查询返回的内容:

{
   "id":1,
   "name":"Dan",
   "email":"someemail@gmail.com",
   "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 很好),并将它们表示为它们加入的父对象的子对象,即如下所示:

{
   "id":1,
   "name":"Dan",
   "email":"someemail@gmail.com",
   "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


【解决方案1】:

更新:在 PostgreSQL 9.4 中,这改进了很多 with the introduction of to_json, json_build_object, json_object and json_build_array,尽管由于需要显式命名所有字段而显得冗长:

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来达到效果:

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/:

{
  "id": 1,
  "name": "Dan",
  "email": "someemail@gmail.com",
  "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:many 关系时,你会想要使用 array_to_json(array_agg(...))

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

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 列别名。很遗憾。

谢天谢地,它们的优化效果相同。比较计划:

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


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

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

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

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

或者:

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

此外,请记住,您可以编写 json 值而无需额外引用,例如如果将json_agg 的输出放在row_to_json 中,则内部json_agg 结果将不会被引用为字符串,它将直接合并为json。

例如在任意示例中:

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
);

输出是:

{"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

【讨论】:

  • 如果我能再对你的答案投票几次,我会的。我很欣赏细节,以及关于 1:many 关系的一点。
  • @dwerner 很高兴为您提供帮助。感谢您努力写出一个好问题;我也想再增加几次it。样本数据、Pg 版本、预期输出、实际输出/错误;勾选所有方框,并且清晰易懂。所以谢谢。
  • @muistooshort:提供类型的临时表也提供服务,并在会话结束时自动删除。
  • 非常感谢您提供的 9.4 示例。 json_build_object 会让我的生活更轻松,但不知何故,当我看到发行说明时,我并没有意识到这一点。有时你只需要一个具体的例子来帮助你开始。
  • 超级答案 - 同意文档应该更多地突出 json_build_object - 这是一个真正的游戏规则改变者。
【解决方案2】:

我添加此解决方案是因为接受的响应不考虑 N:N 关系。又名:对象集合的集合

如果您有 N:N 关系,则子句 with 是您的朋友。 在我的示例中,我想构建以下层次结构的树视图。

A Requirement - Has - TestSuites
A Test Suite - Contains - TestCases.

以下查询表示连接。

SELECT reqId ,r.description as reqDesc ,array_agg(s.id)
            s.id as suiteId , s."Name"  as suiteName,
            tc.id as tcId , tc."Title"  as testCaseTitle

from "Requirement" r 
inner join "Has"  h on r.id = h.requirementid 
inner join "TestSuite" s on s.id  = h.testsuiteid
inner join "Contains" c on c.testsuiteid  = s.id 
inner join "TestCase"  tc on tc.id = c.testcaseid
  GROUP BY r.id, s.id;

由于不能进行多次聚合,所以需要使用“WITH”。

with testcases as (
select  c.testsuiteid,ts."Name" , tc.id, tc."Title"  from "TestSuite" ts
inner join "Contains" c on c.testsuiteid  = ts.id 
inner join "TestCase"  tc on tc.id = c.testcaseid

),                
requirements as (
    select r.id as reqId ,r.description as reqDesc , s.id as suiteId
    from "Requirement" r 
    inner join "Has"  h on r.id = h.requirementid 
    inner join "TestSuite" s on s.id  = h.testsuiteid

    ) 
, suitesJson as (
 select  testcases.testsuiteid,  
       json_agg(
                json_build_object('tc_id', testcases.id,'tc_title', testcases."Title" )
            ) as suiteJson
    from testcases 
    group by testcases.testsuiteid,testcases."Name"
 ),
allSuites as (
    select has.requirementid,
           json_agg(
                json_build_object('ts_id', suitesJson.testsuiteid,'name',s."Name"  , 'test_cases', suitesJson.suiteJson )
            ) as suites
            from suitesJson inner join "TestSuite" s on s.id  = suitesJson.testsuiteid
            inner join "Has" has on has.testsuiteid  = s.id
            group by has.requirementid
),
allRequirements as (
    select json_agg(
            json_build_object('req_id', r.id ,'req_description',r.description , 'test_suites', allSuites.suites )
            ) as suites
            from allSuites inner join "Requirement" r on r.id  = allSuites.requirementid

)
 select * from allRequirements

它的作用是在小项目集合中构建 JSON 对象,并将它们聚合到每个 with 子句中。

结果:

[
  {
    "req_id": 1,
    "req_description": "<character varying>",
    "test_suites": [
      {
        "ts_id": 1,
        "name": "TestSuite",
        "test_cases": [
          {
            "tc_id": 1,
            "tc_title": "TestCase"
          },
          {
            "tc_id": 2,
            "tc_title": "TestCase2"
          }
        ]
      },
      {
        "ts_id": 2,
        "name": "TestSuite",
        "test_cases": [
          {
            "tc_id": 2,
            "tc_title": "TestCase2"
          }
        ]
      }
    ]
  },
  {
    "req_id": 2,
    "req_description": "<character varying> 2 ",
    "test_suites": [
      {
        "ts_id": 2,
        "name": "TestSuite",
        "test_cases": [
          {
            "tc_id": 2,
            "tc_title": "TestCase2"
          }
        ]
      }
    ]
  }
]

【讨论】:

    【解决方案3】:

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

    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 是一个视图。由于我选择了用户。*,如果我需要更新视图以在用户记录中包含更多字段,则无需更新此功能。

    【讨论】:

    • 这一步CAST(ROW(users.*) AS prominence.users) 如有必要,省略表名!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-02-12
    • 2018-10-03
    • 1970-01-01
    • 1970-01-01
    • 2014-10-07
    • 1970-01-01
    • 2013-04-17
    相关资源
    最近更新 更多