PostgreSQL 9.2 row_to_json () z zagnieżdżonymi złączeniami

85

Próbuję zmapować wyniki zapytania do formatu JSON za pomocą row_to_json()funkcji, która została dodana w PostgreSQL 9.2.

Mam problem ze znalezieniem najlepszego sposobu przedstawienia połączonych wierszy jako obiektów zagnieżdżonych (relacje 1: 1)

Oto co wypróbowałem (kod konfiguracji: tabele, przykładowe dane, a następnie zapytanie):

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

Samo zapytanie:

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;

Odkryłem, że jeśli użyję ROW(), mogę oddzielić wynikowe pola na obiekt podrzędny, ale wydaje się, że jest to ograniczone do jednego poziomu. Nie mogę wstawić więcej AS XXXstwierdzeń, ponieważ myślę, że powinienem potrzebować w tym przypadku.

Otrzymuję nazwy kolumn, ponieważ rzutuję na odpowiedni typ rekordu, na przykład z ::user_roles, w przypadku wyników tej tabeli.

Oto, co zwraca to zapytanie:

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

To, co chcę zrobić, to wygenerować JSON dla złączeń (znowu 1: 1 jest w porządku) w sposób, w którym mogę dodawać złączenia i przedstawiać je jako obiekty podrzędne rodziców, do których dołączają, tj. W następujący sposób:

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

Każda pomoc jest mile widziana. Dziękuje za przeczytanie.

dwerner
źródło
1
Znajduje się w kodzie konfiguracji. Wkładki. Zadałem sobie trud skonfigurowania wszystkiego, aby każdy mógł odtworzyć moją sytuację.
dwerner

Odpowiedzi:

161

Aktualizacja: W PostgreSQL 9.4 poprawia wiele z wprowadzeniem to_json, json_build_object, json_objectijson_build_array , choć to gadatliwy ze względu na konieczność, aby wymienić wszystkie jawnie pola:

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;

W przypadku starszych wersji czytaj dalej.


Nie ogranicza się do jednego rzędu, jest po prostu trochę bolesne. Nie możesz aliasować złożonych typów wierszy za pomocą AS, więc musisz użyć aliasu wyrażenia podzapytania lub CTE, aby osiągnąć efekt:

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;

produkuje, za pośrednictwem 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"
    }
  }
}

Będziesz chciał użyć, array_to_json(array_agg(...))gdy masz relację 1: wiele, przy okazji.

Najlepiej byłoby, gdyby powyższe zapytanie można było zapisać jako:

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;

... ale ROWkonstruktor PostgreSQL nie akceptuje ASaliasów kolumn. Niestety.

Na szczęście optymalizują to samo. Porównaj plany:

Ponieważ CTE są barierami optymalizacji, zmiana sformułowania wersji zagnieżdżonego podzapytania w celu użycia połączonych CTE ( WITHwyrażeń) może nie działać tak dobrze i nie spowoduje tego samego planu. W tym przypadku utkniesz z brzydkimi zagnieżdżonymi podzapytaniami, dopóki nie uzyskamy pewnych ulepszeń row_to_jsonlub sposobu na nadpisanie nazw kolumn w ROWkonstruktorze bardziej bezpośrednio.


W każdym razie, ogólnie rzecz biorąc, zasada jest taka, że ​​tam, gdzie chcesz utworzyć obiekt json z kolumnami a, b, c, i chciałbyś po prostu napisać niedozwoloną składnię:

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

zamiast tego możesz użyć podzapytań skalarnych zwracających wartości wpisane w wierszach:

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

Lub:

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

Ponadto pamiętaj, że możesz komponować jsonwartości bez dodatkowego cudzysłowu, np. Jeśli umieścisz wyjście a json_aggwewnątrz a row_to_json, wewnętrzny json_aggwynik nie zostanie cytowany jako łańcuch, zostanie włączony bezpośrednio jako json.

np. w dowolnym przykładzie:

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

wynik to:

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

Zauważ, że json_aggprodukt [{"a":1,"b":2}, {"a":1,"b":2}],, nie został ponownie zmieniony, tak jak textby to było.

Oznacza to, że możesz komponować operacje json w celu tworzenia wierszy, nie zawsze musisz tworzyć bardzo złożone typy złożone PostgreSQL, a następnie wywoływać row_to_jsondane wyjściowe.

Craig Ringer
źródło
2
Gdybym mógł jeszcze kilka razy zagłosować za twoją odpowiedzią, zrobiłbym to. Doceniam szczegóły i trochę relacji 1: wiele.
dwerner
7
@dwerner Cieszę się, że mogłem pomóc. Dziękuję za napisanie dobrego pytania; Chciałbym też podbić to jeszcze kilka razy. Przykładowe dane, wersja Pg, oczekiwany wynik, rzeczywisty wynik / błąd; spełnia wszystkie wymagania, jest jasny i łatwy do zrozumienia. Więc dziękuję.
Craig Ringer
1
@muistooshort: tabela tymczasowa zawierająca typ służy również i jest usuwana automatycznie po zakończeniu sesji.
Erwin Brandstetter
1
Bardzo dziękuję za przykład 9.4. json_build_objectznacznie ułatwi mi życie, ale jakoś nie zauważyłem, kiedy zobaczyłem informacje o wydaniu. Czasami potrzebujesz konkretnego przykładu, aby zacząć.
Jeff
1
Super odpowiedź - zgódź się, że dokumentacja powinna podkreślać json_build_objectnieco więcej - to prawdziwa zmiana gry.
bobmarksie
1

Moja sugestia dotycząca łatwości utrzymania w dłuższej perspektywie to użycie VIEW do zbudowania przybliżonej wersji zapytania, a następnie użycie funkcji jak poniżej:

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;

W tym przypadku obiekt prominence.users jest widokiem. Ponieważ wybrałem użytkowników. *, Nie będę musiał aktualizować tej funkcji, jeśli będę musiał zaktualizować widok, aby uwzględnić więcej pól w rekordzie użytkownika.

Todd
źródło
1

Dodam to rozwiązanie, ponieważ przyjęta odpowiedź nie uwzględnia relacji N: N. aka: kolekcje zbiorów obiektów

Jeśli masz relacje N: N, klauzula withto twój przyjaciel. W moim przykładzie chciałbym zbudować widok drzewa o następującej hierarchii.

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

Poniższe zapytanie reprezentuje sprzężenia.

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;

Ponieważ nie możesz wykonać wielu agregacji, musisz użyć „Z”.

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

To, co robi, to budowanie obiektu JSON w małej kolekcji elementów i agregowanie ich w każdej withklauzuli.

Wynik:

[
  {
    "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
źródło