PostgreSQL 9.2 row_to_json () com junções aninhadas


85

Estou tentando mapear os resultados de uma consulta para JSON usando a row_to_json()função que foi adicionada no PostgreSQL 9.2.

Estou tendo problemas para descobrir a melhor maneira de representar linhas unidas como objetos aninhados (relações 1: 1)

Aqui está o que eu tentei (código de configuração: tabelas, dados de amostra, seguidos de consulta):

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

A própria consulta:

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;

Descobri que, se usasse ROW(), poderia separar os campos resultantes em um objeto filho, mas parece limitado a um único nível. Não consigo inserir mais AS XXXinstruções, pois acho que deveria precisar neste caso.

Recebo nomes de colunas, porque faço a conversão para o tipo de registro apropriado, por exemplo ::user_roles, com , no caso dos resultados dessa tabela.

Aqui está o que essa consulta retorna:

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

O que eu quero fazer é gerar JSON para junções (novamente 1: 1 é bom) de uma forma onde eu possa adicionar junções e tê-los representados como objetos filhos dos pais aos quais eles se juntam, ou seja, como o seguinte:

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

Qualquer ajuda é apreciada. Obrigado pela leitura.


1
Está lá no código de configuração. As inserções. Tive o trabalho de configurar tudo para que qualquer um pudesse replicar minha situação.
dwerner

Respostas:


164

Update: No PostgreSQL 9.4 este melhora muito com a introdução de to_json, json_build_object, json_objectejson_build_array , embora seja detalhado devido à necessidade de nomear todos os campos explicitamente:

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;

Para versões mais antigas, continue lendo.


Não se limita a uma única linha, é um pouco doloroso. Você não pode criar um alias de tipos de linhas compostos usando AS, portanto, é necessário usar uma expressão de subconsulta com alias ou CTE para obter o efeito:

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;

produz, via 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"
    }
  }
}

Você vai querer usar array_to_json(array_agg(...))quando tiver um relacionamento 1: muitos, aliás.

Idealmente, a consulta acima deve ser capaz de ser escrita como:

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;

... mas o ROWconstrutor do PostgreSQL não aceita ASapelidos de coluna. Infelizmente.

Felizmente, eles otimizam o mesmo. Compare os planos:

Como os CTEs são cercas de otimização, reformular a versão da subconsulta aninhada para usar CTEs ( WITHexpressões) encadeados pode não funcionar tão bem e não resultará no mesmo plano. Nesse caso, você está preso a subconsultas aninhadas feias até obtermos algumas melhorias row_to_jsonou uma maneira de substituir os nomes das colunas em um ROWconstrutor mais diretamente.


De qualquer forma, em geral, o princípio é que você deseja criar um objeto json com colunas a, b, ce deseja apenas escrever a sintaxe ilegal:

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

em vez disso, você pode usar subconsultas escalares que retornam valores digitados em linha:

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

Ou:

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

Além disso, tenha em mente que você pode compor jsonvalores sem aspas adicionais, por exemplo, se você colocar a saída de a json_aggdentro de a row_to_json, o json_aggresultado interno não será aspas como uma string, ele será incorporado diretamente como json.

por exemplo, no exemplo arbitrário:

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

a saída é:

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

Observe que o json_aggproduto [{"a":1,"b":2}, {"a":1,"b":2}],, não foi escapado novamente, como textseria.

Isso significa que você pode compor operações json para construir linhas, nem sempre é necessário criar tipos compostos PostgreSQL extremamente complexos e, row_to_jsonem seguida, chamar a saída.


2
Se eu pudesse votar a favor de sua resposta mais algumas vezes, eu o faria. Agradeço os detalhes e a parte sobre 1: muitos relacionamentos.
dwerner

7
@dwerner Fico feliz em ajudar. Obrigado por se esforçar para escrever uma boa pergunta; Eu gostaria de bater -lo até mais algumas vezes também. Dados de amostra, versão Pg, saída esperada, saída real / erro; preenche todas as caixas e é claro e fácil de entender. Então, obrigado.
Craig Ringer

1
@muistooshort: Uma tabela temporária para fornecer o tipo também serve e é excluída automaticamente no final da sessão.
Erwin Brandstetter,

1
Muito obrigado pelo exemplo 9.4. json_build_objectvai tornar minha vida muito mais fácil, mas de alguma forma eu não percebi quando vi as notas de lançamento. Às vezes, você só precisa de um exemplo concreto para começar.
Jeff

1
Super resposta - concordo que a documentação deve destacar json_build_objectum pouco mais - é uma verdadeira virada de jogo.
bobmarksie

2

Estou adicionando esta solução porque a resposta aceita não contempla relacionamentos N: N. aka: coleções de coleções de objetos

Se você tem relacionamentos N: N, a cláusula withé seu amigo. No meu exemplo, gostaria de construir uma visualização em árvore da seguinte hierarquia.

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

A consulta a seguir representa as junções.

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;

Como você não pode fazer múltiplas agregações, você precisa usar "COM".

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

O que ele faz é construir o objeto JSON em uma pequena coleção de itens e agregá-los em cada withcláusula.

Resultado:

[
  {
    "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"
          }
        ]
      }
    ]
  }
]

1

Minha sugestão para sustentabilidade em longo prazo é usar VIEW para construir a versão grosseira de sua consulta e, em seguida, usar uma função como abaixo:

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;

Nesse caso, o objeto prominence.users é uma visão. Como selecionei usuários. *, Não terei que atualizar esta função se precisar atualizar a visualização para incluir mais campos em um registro de usuário.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.