PostgreSQL 9.2 row_to_json () mit verschachtelten Joins

84

Ich versuche, die Ergebnisse einer Abfrage mithilfe der row_to_json()in PostgreSQL 9.2 hinzugefügten Funktion JSON zuzuordnen .

Ich habe Probleme, herauszufinden, wie verbundene Zeilen am besten als verschachtelte Objekte dargestellt werden können (1: 1-Beziehungen).

Folgendes habe ich versucht (Setup-Code: Tabellen, Beispieldaten, gefolgt von einer Abfrage):

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

Die Abfrage selbst:

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;

Ich habe festgestellt, dass ich bei Verwendung ROW()die resultierenden Felder in ein untergeordnetes Objekt aufteilen könnte, aber es scheint auf eine einzelne Ebene beschränkt zu sein. Ich kann keine weiteren AS XXXAnweisungen einfügen , da ich denke, dass ich sie in diesem Fall benötigen sollte.

Ich erhalte Spaltennamen, weil ich in den entsprechenden Datensatztyp umgewandelt habe, z. B. mit ::user_rolesim Fall der Ergebnisse dieser Tabelle.

Diese Abfrage gibt Folgendes zurück:

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

Was ich tun möchte, ist JSON für Joins zu generieren (wieder 1: 1 ist in Ordnung), so dass ich Joins hinzufügen und sie als untergeordnete Objekte der Eltern darstellen lassen kann, denen sie beitreten, dh wie folgt:

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

Jede Hilfe wird geschätzt. Danke fürs Lesen.

Dwerner
quelle
1
Es ist dort im Setup-Code. Die Einsätze. Ich machte mir die Mühe, alles so einzurichten, dass jeder meine Situation wiederholen konnte.
Dwerner

Antworten:

161

Update: In PostgreSQL 9.4 verbessert dies viel mit der Einführung von to_json, json_build_object, json_objectundjson_build_array , obwohl es ausführliche aufgrund der Notwendigkeit , alle Felder zu nennen ausdrücklich:

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;

Lesen Sie für ältere Versionen weiter.


Es ist nicht auf eine einzelne Reihe beschränkt, es ist nur ein bisschen schmerzhaft. Sie können zusammengesetzte Zeilentypen nicht als Alias ​​verwenden AS, daher müssen Sie einen Alias-Unterabfrageausdruck oder CTE verwenden, um den Effekt zu erzielen:

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;

produziert über 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"
    }
  }
}

Sie werden verwenden wollen, array_to_json(array_agg(...))wenn Sie eine 1: viele-Beziehung haben, übrigens.

Die obige Abfrage sollte idealerweise wie folgt geschrieben werden können:

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;

... aber der ROWKonstruktor von PostgreSQL akzeptiert keine ASSpaltenaliasnamen. Traurig.

Zum Glück optimieren sie das gleiche. Vergleichen Sie die Pläne:

Da CTEs Optimierungszäune sind, funktioniert die Neuformulierung der verschachtelten Unterabfrageversion zur Verwendung verketteter CTEs ( WITHAusdrücke) möglicherweise nicht so gut und führt nicht zum gleichen Plan. In diesem Fall bleiben Sie bei hässlichen verschachtelten Unterabfragen hängen, bis wir einige Verbesserungen row_to_jsonoder eine Möglichkeit erhalten, die Spaltennamen in einem ROWKonstruktor direkter zu überschreiben .


Im Allgemeinen besteht das Prinzip darin, dass Sie dort, wo Sie ein JSON-Objekt mit Spalten erstellen möchten a, b, cund nur die unzulässige Syntax schreiben möchten:

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

Sie können stattdessen skalare Unterabfragen verwenden, die zeilentypisierte Werte zurückgeben:

(SELECT x FROM (SELECT a AS name1, b AS name2, c AS name3) x) AS outername

Oder:

(SELECT x FROM (SELECT a, b, c) AS x(name1, name2, name3)) AS outername

Beachten Sie außerdem, dass Sie jsonWerte ohne zusätzliches Anführungszeichen erstellen können. Wenn Sie beispielsweise die Ausgabe von a json_aggin a einfügen, row_to_jsonwird das innere json_aggErgebnis nicht als Zeichenfolge in Anführungszeichen gesetzt, sondern direkt als json eingefügt.

zB im willkürlichen Beispiel:

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

Die Ausgabe ist:

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

Beachten Sie, dass das json_aggProdukt [{"a":1,"b":2}, {"a":1,"b":2}]nicht wie gewünscht erneut textmaskiert wurde.

Dies bedeutet, dass Sie JSON-Operationen erstellen können , um Zeilen zu erstellen. Sie müssen nicht immer äußerst komplexe PostgreSQL-Verbundtypen erstellen und dann row_to_jsondie Ausgabe aufrufen .

Craig Ringer
quelle
2
Wenn ich Ihre Antwort noch ein paar Mal positiv bewerten könnte, würde ich es tun. Ich schätze das Detail und das bisschen über 1: viele Beziehungen.
Dwerner
7
@ Dwerner Gerne helfen. Vielen Dank, dass Sie sich die Mühe gemacht haben, eine gute Frage zu schreiben. Ich möchte stoßen sie noch ein paar Mal zu hoch. Beispieldaten, Pg-Version, erwartete Ausgabe, tatsächliche Ausgabe / Fehler; kreuzt alle Kästchen an und ist klar und leicht zu verstehen. So danke.
Craig Ringer
1
@muistooshort: Eine temporäre Tabelle zur Bereitstellung des Typs dient ebenfalls und wird am Ende der Sitzung automatisch gelöscht.
Erwin Brandstetter
1
Vielen Dank für das Beispiel 9.4. json_build_objectwird mein Leben viel einfacher machen, aber irgendwie habe ich es nicht verstanden, als ich die Versionshinweise sah. Manchmal brauchen Sie nur ein konkretes Beispiel, um loszulegen.
Jeff
1
Super Antwort - stimme zu, dass die Dokumentation json_build_objectetwas mehr hervorheben sollte - es ist ein echter Game Changer.
Bobmarksie
1

Mein Vorschlag für eine langfristige Wartbarkeit besteht darin, eine VIEW zu verwenden, um die grobe Version Ihrer Abfrage zu erstellen, und dann eine der folgenden Funktionen zu verwenden:

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 diesem Fall ist das Objekt prominence.users eine Ansicht. Da ich Benutzer ausgewählt habe. *, Muss ich diese Funktion nicht aktualisieren, wenn ich die Ansicht aktualisieren muss, um mehr Felder in einen Benutzerdatensatz aufzunehmen.

Todd
quelle
1

Ich füge diese Lösung hinzu, da die akzeptierte Antwort keine N: N-Beziehungen berücksichtigt. aka: Sammlungen von Sammlungen von Objekten

Wenn Sie N: N-Beziehungen haben, ist die Klausel withIhr Freund. In meinem Beispiel möchte ich eine Baumansicht der folgenden Hierarchie erstellen.

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

Die folgende Abfrage repräsentiert die Verknüpfungen.

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;

Da Sie nicht mehrere Aggregationen durchführen können, müssen Sie "WITH" verwenden.

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

Dabei wird das JSON-Objekt in einer kleinen Sammlung von Elementen erstellt und auf den einzelnen withKlauseln zusammengefasst.

Ergebnis:

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