PostgreSQL 9.2 row_to_json () con combinaciones anidadas

85

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 XXXdeclaraciones, 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.

dwerner
fuente
1
Está ahí en el código de configuración. Las inserciones. Me tomé la molestia de configurar todo para que cualquiera pudiera replicar mi situación.
dwerner

Respuestas:

161

Actualización: En PostgreSQL 9.4 esto mejora mucho con la introducción de to_json, json_build_object, json_objectyjson_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 ASusar 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/ :

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

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 ROWconstructor de PostgreSQL no acepta ASalias de columna. Tristemente.

Afortunadamente, optimizan lo mismo. Compare los planes:

Debido a que las CTE son vallas de optimización, es posible que reformular la versión de la subconsulta anidada para usar CTE ( WITHexpresiones) 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 mejoras row_to_jsono una forma de anular los nombres de columna en un ROWconstructor 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:

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

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 jsonvalores sin comillas adicionales, por ejemplo, si coloca la salida de a json_aggdentro de a row_to_json, el json_aggresultado 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:

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

Tenga en cuenta que el json_aggproducto`` [{"a":1,"b":2}, {"a":1,"b":2}]no se ha vuelto a escapar, como textserí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_jsona la salida.

Craig Ringer
fuente
2
Si pudiera votar su respuesta un par de veces más, lo haría. Aprecio el detalle y la parte de 1: muchas relaciones.
dwerner
7
@dwerner Encantado de ayudar. Gracias por hacer el esfuerzo de escribir una buena pregunta; Me gustaría toparse que unas cuantas más veces demasiado. Datos de muestra, versión Pg, salida esperada, salida / error real; cumple todos los requisitos y es claro y fácil de entender. Así que gracias.
Craig Ringer
1
@muistooshort: una tabla temporal para proporcionar el tipo sirve también, y se elimina automáticamente al final de la sesión.
Erwin Brandstetter
1
Muchas gracias por el ejemplo 9.4. json_build_objectva 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.
Jeff
1
Súper respuesta: estoy de acuerdo en que la documentación debería resaltar json_build_objectun poco más, es un verdadero cambio de juego.
bobmarksie
1

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.

Todd
fuente
1

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 withes tu amiga. En mi ejemplo, me gustaría construir una vista de árbol de la siguiente jerarquía.

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

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 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"
          }
        ]
      }
    ]
  }
]
Gonzalo del cerro
fuente