Saltar al contenido

PostgreSQL 9.2 row_to_json () con combinaciones anidadas

Solución:

Actualización: en PostgreSQL 9.4 esto mejora mucho con la introducción de to_json, json_build_object, json_object y json_build_array, aunque es detallado debido a la necesidad de nombrar 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 crear un alias de tipos de filas compuestos usando AS, 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ás usar array_to_json(array_agg(...)) cuando tienes 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 PostgreSQL ROW el constructor no acepta AS alias de columna. Desafortunadamente.

Afortunadamente, optimizan lo mismo. Compare los planes:

  • La versión de la subconsulta anidada; vs
  • Este último anidado ROW versión del constructor con los alias eliminados para que se ejecute

Debido a que los CTE son vallas de optimización, reformular la versión de subconsultas anidadas para usar CTE encadenados (WITH expresiones) pueden no funcionar tan bien y no darán como resultado el mismo plan. En este caso, está atascado con subconsultas anidadas desagradables hasta que obtengamos algunas mejoras en row_to_json o una forma de anular los nombres de las columnas en un ROW constructor más directamente.


De todos modos, en general, el principio es que donde desea crear un objeto json con columnas a, b, c, y desearía 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 json valores sin comillas adicionales, por ejemplo, si coloca la salida de un json_agg Dentro de un row_to_json, el interior json_agg el resultado no se cotizará 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_agg producto, [{"a":1,"b":2}, {"a":1,"b":2}], no se ha escapado de nuevo, ya que text sería.

Esto significa que puedes componer json para construir filas, no siempre es necesario crear tipos compuestos de PostgreSQL enormemente complejos y luego llamar row_to_json en la salida.

Mi sugerencia para la capacidad de mantenimiento 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. Como seleccioné users. *, No tendré que actualizar esta función si necesito actualizar la vista para incluir más campos en un registro de usuario.

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 amigo. 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 “CON”.

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 clausulas.

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"
          }
        ]
      }
    ]
  }
]
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *