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

Любая помощь приветствуется. Спасибо за прочтение.

Dwerner
источник
1
Это есть в установочном коде. Вставки. Я потрудился настроить все так, чтобы любой мог воспроизвести мою ситуацию.
dwerner 05

Ответы:

164

Обновление: В 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 05
7
@dwerner Рад помочь. Спасибо, что постарались написать хороший вопрос; Я бы тоже хотел поднять его еще несколько раз. Образцы данных, версия Pg, ожидаемый результат, фактический результат / ошибка; ставит галочки во всех полях, он ясен и прост для понимания. Тогда спасибо.
Craig Ringer
1
@muistooshort: временная таблица для предоставления типа тоже обслуживается и автоматически удаляется в конце сеанса.
Эрвин Брандштеттер,
1
Большое спасибо за пример 9.4. json_build_objectсделает мою жизнь намного проще, но почему-то я не уловил этого, когда увидел примечания к выпуску. Иногда для начала вам просто нужен конкретный пример.
Джефф
1
Супер ответ - согласитесь, что документация должна выделять json_build_objectнемного больше - это настоящий переломный момент.
bobmarksie
2

Я добавляю это решение, потому что принятый ответ не предполагает отношения N: N. иначе: коллекции коллекций объектов

Если у вас есть отношения N: N, то clausula - 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"
          }
        ]
      }
    ]
  }
]
Гонсало дель серро
источник
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 - это представление. Так как я выбрал users. *, Мне не придется обновлять эту функцию, если мне нужно обновить представление, чтобы включить больше полей в запись пользователя.

Тодд
источник