PostgreSQL 9.2 row_to_json () із вкладеними об'єднаннями


85

Я намагаюся відобразити результати запиту в JSON, використовуючи row_to_json()функцію, додану в PostgreSQL 9.2.

У мене виникають проблеми з визначенням найкращого способу представити об’єднані рядки як вкладені об’єкти (відношення 1: 1)

Ось те, що я спробував (код налаштування: таблиці, зразки даних, а потім запит):

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

Сам запит:

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;

Я виявив, що якщо б використовував ROW(), я міг би розділити отримані поля на дочірній об’єкт, але, здається, він обмежений одним рівнем. Я не можу вставити більше AS XXXвисловлювань, оскільки вважаю, що мені потрібно було б у цьому випадку.

Мені надаються імена стовпців, оскільки я призначаю відповідний тип запису, наприклад ::user_roles, у випадку результатів цієї таблиці.

Ось що повертає цей запит:

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

Що я хочу зробити, це генерувати JSON для об'єднань (знову 1: 1 це нормально) таким чином, щоб я міг додавати об'єднання і представляти їх як дочірні об'єкти батьків, до яких вони приєднуються, тобто, наприклад:

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

Будь-яка допомога вдячна. Дякуємо за читання.


1
Це є в коді налаштування. Вставки. Я зіткнувся з проблемою, щоб все налаштувати, щоб кожен міг повторити мою ситуацію.
dwerner

Відповіді:


161

Оновлення: У PostgreSQL 9.4 це покращує багато з введенням to_json, json_build_object, json_objectіjson_build_array , хоча це багатослівним з - за необхідності назвати все поля в явному вигляді :

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;

Для старих версій читайте далі.


Це не обмежується одним рядком, це просто трохи боляче. Ви не можете використовувати псевдонім складених типів рядків AS, тому для досягнення ефекту вам потрібно використовувати вираз псевдоніму підзапиту або CTE:

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;

виробляє через 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"
    }
  }
}

Ви захочете використовувати, array_to_json(array_agg(...))коли у вас стосунки 1: багато, до речі.

Наведений вище запит в ідеалі повинен мати можливість писати як:

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;

... але ROWконструктор PostgreSQL не приймає ASпсевдоніми стовпців. На жаль

На щастя, вони оптимізують те саме. Порівняйте плани:

Оскільки CTE є оптимізаційними огородженнями, перефразування вкладеної версії підзапиту для використання ланцюгових CTE ( WITHвиразів) може не працювати так само, і не призведе до того самого плану. У цьому випадку ви начебто застрягли в потворних вкладених підзапитах, поки ми не отримаємо певних удосконалень row_to_jsonабо способу перевизначення імен стовпців у ROWконструкторі більш безпосередньо.


У будь-якому випадку, загалом, принцип полягає в тому, що там, де ви хочете створити json-об'єкт зі стовпцями a, b, c, і ви хотіли б просто написати нелегальний синтаксис:

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

замість цього ви можете використовувати скалярні підзапити, що повертають значення, введені в рядки:

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

Або:

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

Крім того, майте на увазі, що ви можете складати jsonзначення без додаткового цитування, наприклад, якщо ви введете висновок a json_aggв межах a row_to_json, внутрішній json_aggрезультат не буде цитуватися як рядок, він буде включений безпосередньо як json.

наприклад, у довільному прикладі:

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

Вихід:

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

Зверніть увагу, що json_aggпродукт, [{"a":1,"b":2}, {"a":1,"b":2}]ще раз не захищений, як це textбуло б.

Це означає, що ви можете складати json-операції для побудови рядків, вам не завжди потрібно створювати надзвичайно складні композитні типи PostgreSQL, а потім викликати row_to_jsonна виході.


2
Якби я міг підтримати вашу відповідь ще пару разів, я б. Я ціную деталі, і трохи про 1: багато стосунків.
dwerner

7
@dwerner Радий допомогти. Дякую, що доклали зусиль для написання гарного запитання; Я також хотів би підняти це ще кілька разів. Зразкові дані, версія Pg, очікуваний вихід, фактичний вихід / помилка; ставить галочки на всіх полях, і це чітко і легко зрозуміти. Тож спасибі.
Крейг Рінгер,

1
@muistooshort: тимчасова таблиця, яка також надає тип, і видаляється автоматично в кінці сеансу.
Ервін Брандштеттер

1
Щиро дякую за приклад 9.4. json_build_objectзробить моє життя набагато простішим, але якось я не зрозумів його, коли побачив нотатки до випуску. Іноді для початку вам потрібен лише конкретний приклад.
Джефф,

1
Супер відповідь - погодьтеся, що в документації слід виділити json_build_objectтрохи більше - це справжня зміна гри.
bobmarksie

1

Моя порада щодо ремонтопридатності в довгостроковій перспективі - використовувати VIEW для побудови грубої версії вашого запиту, а потім використовувати функцію, як показано нижче:

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;

У цьому випадку об’єктом prominence.users є подання. Оскільки я вибрав користувачів. *, Мені не доведеться оновлювати цю функцію, якщо мені потрібно оновити подання, щоб включити більше полів в запис користувача.


1

Я додаю це рішення, оскільки прийнята відповідь не передбачає відносин N: N. aka: колекції колекцій предметів

Якщо у вас стосунки N: N, то клаузула - withце ваш друг. У своєму прикладі я хотів би побудувати деревоподібне представлення такої ієрархії.

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

Наступний запит представляє об'єднання.

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;

Оскільки ви не можете зробити кілька агрегувань, вам потрібно використовувати "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

Він створює об’єкт JSON у невеликій колекції предметів та агрегує їх за кожною withклаузулою.

Результат:

[
  {
    "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"
          }
        ]
      }
    ]
  }
]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.