Estoy tratando de mapear los resultados de una consulta a JSON usando la row_to_json()
función que se agregó en PostgreSQL 9.2.
Tengo problemas para descubrir la mejor manera de representar filas unidas como objetos anidados (relaciones 1: 1)
Esto es lo que probé (código de configuración: tablas, datos de muestra, 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$$;
La consulta en sí:
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;
Descubrí que si lo usaba ROW()
, podría separar los campos resultantes en un objeto secundario, pero parece limitado a un solo nivel. No puedo insertar más AS XXX
declaraciones, ya que creo que debería necesitar en este caso.
Se me otorgan nombres de columna, porque lanzo al tipo de registro apropiado, por ejemplo ::user_roles
, con , en el caso de los resultados de esa tabla.
Esto es lo que devuelve esa consulta:
{
"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"
}
}
}
}
Lo que quiero hacer es generar JSON para uniones (nuevamente 1: 1 está bien) de una manera en la que pueda agregar uniones y representarlas como objetos secundarios de los padres a los que se unen, es decir, como se muestra a continuación:
{
"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"
}
}
}
}
Se agradece cualquier ayuda. Gracias por leer.
Respuestas:
Actualización: En PostgreSQL 9.4 esto mejora mucho con la introducción de
to_json
,json_build_object
,json_object
yjson_build_array
, aunque es prolijo, debido a la necesidad de nombrar a todos los campos explícitamente: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 versiones anteriores, siga leyendo.
No se limita a una sola fila, solo es un poco doloroso. No puede
AS
usar un alias de tipos de filas compuestos , por lo que debe usar una expresión de subconsulta con alias o CTE para lograr el efecto: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;
produce, a través de http://jsonprettyprint.com/ :
Querrá usarlo
array_to_json(array_agg(...))
cuando tenga una relación de 1: muchos, por cierto.Idealmente, la consulta anterior debería poder escribirse 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;
... pero el
ROW
constructor de PostgreSQL no aceptaAS
alias de columna. Tristemente.Afortunadamente, optimizan lo mismo. Compare los planes:
ROW
con los alias eliminados para que se ejecuteDebido a que las CTE son vallas de optimización, es posible que reformular la versión de la subconsulta anidada para usar CTE (
WITH
expresiones) encadenadas no funcione tan bien y no resulte en el mismo plan. En este caso, está un poco atrapado con subconsultas anidadas desagradables hasta que obtengamos algunas mejorasrow_to_json
o una forma de anular los nombres de columna en unROW
constructor de manera más directa.De todos modos, en general, el principio es que cuando desea crear un objeto json con columnas
a, b, c
, y desea poder escribir la sintaxis ilegal:en su lugar, puede usar subconsultas escalares que devuelvan valores con tipo de fila:
(SELECT x FROM (SELECT a AS name1, b AS name2, c AS name3) x) AS outername
O:
(SELECT x FROM (SELECT a, b, c) AS x(name1, name2, name3)) AS outername
Además, tenga en cuenta que puede componer
json
valores sin comillas adicionales, por ejemplo, si coloca la salida de ajson_agg
dentro de arow_to_json
, eljson_agg
resultado interno no se citará como una cadena, se incorporará directamente como json.por ejemplo, en el ejemplo arbitrario:
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 );
la salida es:
Tenga en cuenta que el
json_agg
producto``[{"a":1,"b":2}, {"a":1,"b":2}]
no se ha vuelto a escapar, comotext
sería.Esto significa que puede componer operaciones json para construir filas, no siempre tiene que crear tipos compuestos de PostgreSQL enormemente complejos y luego llamar
row_to_json
a la salida.fuente
json_build_object
va a hacer mi vida mucho más fácil, pero de alguna manera no me di cuenta cuando vi las notas de la versión. A veces, solo necesitas un ejemplo concreto para comenzar.json_build_object
un poco más, es un verdadero cambio de juego.Mi sugerencia para la mantenibilidad a largo plazo es usar una VISTA para construir la versión aproximada de su consulta, y luego usar una función como la siguiente:
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;
En este caso, el objeto prominence.users es una vista. Dado que seleccioné usuarios. *, No tendré que actualizar esta función si necesito actualizar la vista para incluir más campos en un registro de usuario.
fuente
Estoy agregando esta solución porque la respuesta aceptada no contempla las relaciones N: N. también conocido como: colecciones de colecciones de objetos
Si tienes relaciones N: N, la clausula
with
es tu amiga. En mi ejemplo, me gustaría construir una vista de árbol de la siguiente jerarquía.La siguiente consulta representa las combinaciones.
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;
Dado que no puede hacer múltiples agregaciones, debe usar "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
Lo que hace es construir el objeto JSON en una pequeña colección de elementos y agregarlos en cada
with
cláusula.Resultado:
fuente