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', '[email protected]', 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 XXX
instruçõ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":"[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"
}
}
}
}
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":"[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"
}
}
}
}
Qualquer ajuda é apreciada. Obrigado pela leitura.
fonte
Respostas:
Update: No PostgreSQL 9.4 este melhora muito com a introdução de
to_json
,json_build_object
,json_object
ejson_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/ :
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
ROW
construtor do PostgreSQL não aceitaAS
apelidos de coluna. Infelizmente.Felizmente, eles otimizam o mesmo. Compare os planos:
ROW
com os apelidos removidos para que ele executeComo os CTEs são cercas de otimização, reformular a versão da subconsulta aninhada para usar CTEs (
WITH
expressõ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 melhoriasrow_to_json
ou uma maneira de substituir os nomes das colunas em umROW
construtor mais diretamente.De qualquer forma, em geral, o princípio é que você deseja criar um objeto json com colunas
a, b, c
e deseja apenas escrever a sintaxe ilegal: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
json
valores sem aspas adicionais, por exemplo, se você colocar a saída de ajson_agg
dentro de arow_to_json
, ojson_agg
resultado 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 é:
Observe que o
json_agg
produto[{"a":1,"b":2}, {"a":1,"b":2}]
,, não foi escapado novamente, comotext
seria.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_json
em seguida, chamar a saída.fonte
json_build_object
vai 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.json_build_object
um pouco mais - é uma verdadeira virada de jogo.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 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
with
cláusula.Resultado:
fonte
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.
fonte