PostgreSQL 9.2 row_to_json () con join nidificati


85

Sto cercando di mappare i risultati di una query su JSON utilizzando la row_to_json()funzione aggiunta in PostgreSQL 9.2.

Ho problemi a capire il modo migliore per rappresentare le righe unite come oggetti nidificati (relazioni 1: 1)

Ecco cosa ho provato (codice di installazione: tabelle, dati di esempio, seguito da query):

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

La query stessa:

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;

Ho scoperto che se lo usassi ROW(), potrei separare i campi risultanti in un oggetto figlio, ma sembra limitato a un singolo livello. Non posso inserire più AS XXXdichiarazioni, perché penso che dovrei aver bisogno in questo caso.

Mi vengono forniti i nomi delle colonne, perché eseguo il cast sul tipo di record appropriato, ad esempio con ::user_roles, nel caso dei risultati di quella tabella.

Ecco cosa restituisce la query:

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

Quello che voglio fare è generare JSON per i join (di nuovo 1: 1 va bene) in un modo in cui posso aggiungere join e rappresentarli come oggetti figlio dei genitori a cui si uniscono, ovvero come il seguente:

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

Qualsiasi aiuto è apprezzato. Grazie per aver letto.


1
È presente nel codice di configurazione. Gli inserti. Mi sono preso la briga di impostare tutto in modo che chiunque potesse replicare la mia situazione.
calo il

Risposte:


164

Aggiornamento: In PostgreSQL 9.4 questo migliora molto con l'introduzione di to_json, json_build_object, json_objectejson_build_array , anche se è verbose a causa della necessità di nominare tutti i campi esplicitamente:

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;

Per le versioni precedenti, continua a leggere.


Non è limitato a una singola riga, è solo un po 'doloroso. Non è possibile creare l'alias di tipi di riga compositi utilizzando AS, quindi è necessario utilizzare un'espressione di sottoquery con alias o CTE per ottenere l'effetto:

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, tramite 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"
    }
  }
}

Ti consigliamo di usarlo array_to_json(array_agg(...))quando hai una relazione 1: molti, btw.

La query sopra dovrebbe idealmente essere in grado di essere scritta come:

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;

... ma il ROWcostruttore di PostgreSQL non accetta ASalias di colonna. Purtroppo.

Per fortuna, ottimizzano lo stesso. Confronta i piani:

Poiché i CTE sono recinti di ottimizzazione, riformulare la versione della sottoquery annidata per utilizzare CTE concatenati ( WITHespressioni) potrebbe non funzionare altrettanto bene e non risulterà nello stesso piano. In questo caso sei bloccato con brutte sottoquery annidate fino a quando non otteniamo alcuni miglioramenti row_to_jsono un modo per sovrascrivere ROWpiù direttamente i nomi delle colonne in un costruttore.


Ad ogni modo, in generale, il principio è che dove vuoi creare un oggetto json con colonne a, b, ce desideri che tu possa semplicemente scrivere la sintassi illegale:

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

puoi invece utilizzare sottoquery scalari che restituiscono valori tipizzati da riga:

(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

Inoltre, tieni presente che puoi comporre jsonvalori senza virgolette aggiuntive, ad esempio se metti l'output di a json_aggall'interno di a row_to_json, il json_aggrisultato interno non verrà citato come stringa, verrà incorporato direttamente come json.

ad es. nell'esempio 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
);

l'output è:

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

Si noti che il json_aggprodotto [{"a":1,"b":2}, {"a":1,"b":2}],, non è stato nuovamente sottoposto a escape, come textsarebbe.

Ciò significa che puoi comporre operazioni JSON per costruire righe, non devi sempre creare tipi compositi PostgreSQL estremamente complessi e poi chiamare row_to_jsonl'output.


2
Se potessi votare la tua risposta ancora un paio di volte, lo farei. Apprezzo i dettagli e la parte su 1: molte relazioni.
calo il

7
@dwerner Felice di aiutare. Grazie per aver fatto lo sforzo di scrivere una buona domanda; Mi piacerebbe anche aumentarlo ancora un paio di volte. Dati di esempio, versione PG, output previsto, output / errore effettivo; soddisfa tutte le esigenze ed è chiaro e di facile comprensione. Quindi grazie.
Craig Ringer

1
@muistooshort: una tabella temporanea per fornire anche il tipo serve e viene eliminata automaticamente alla fine della sessione.
Erwin Brandstetter

1
Grazie mille per l'esempio 9.4. json_build_objectrenderà la mia vita molto più facile ma in qualche modo non l'ho capito quando ho visto le note di rilascio. A volte hai solo bisogno di un esempio concreto per iniziare.
Jeff

1
Super risposta: d'accordo sul fatto che la documentazione dovrebbe evidenziare json_build_objectun po 'di più: è un vero punto di svolta.
bobmarksie

2

Aggiungo questa soluzione perché la risposta accettata non contempla relazioni N: N. aka: collezioni di collezioni di oggetti

Se hai relazioni N: N la clausola withè tua amica. Nel mio esempio, vorrei creare una visualizzazione ad albero della seguente gerarchia.

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

La seguente query rappresenta i join.

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;

Poiché non è possibile eseguire aggregazioni multiple, è necessario utilizzare "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

Quello che fa è costruire l'oggetto JSON in una piccola raccolta di elementi e aggregarli su ciascuna withclausola.

Risultato:

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

Il mio suggerimento per la manutenibilità a lungo termine è di utilizzare una VISTA per creare la versione grossolana della query, quindi utilizzare una funzione come di seguito:

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;

In questo caso, l'oggetto prominence.users è una vista. Poiché ho selezionato gli utenti. *, Non dovrò aggiornare questa funzione se devo aggiornare la vista per includere più campi in un record utente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.