PostgreSQL 9.2 row_to_json () с вложенными объединениями

Я пытаюсь сопоставить результаты запроса с 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', '[email protected]', 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":"[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"
         }
      }
   }
}

Что я хочу сделать, так это сгенерировать JSON для объединений (опять же, 1: 1 в порядке) таким образом, чтобы я мог добавлять объединения и представлять их как дочерние объекты родителей, к которым они присоединяются, то есть следующим образом:

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

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


person dwerner    schedule 05.11.2012    source источник
comment
Это есть в установочном коде. Вкладыши. Я потрудился настроить все так, чтобы любой мог повторить мою ситуацию.   -  person dwerner    schedule 05.11.2012


Ответы (3)


Обновление: в PostgreSQL 9.4 это значительно улучшило с введением to_json, json_build_object, json_object и _4 _ , хотя он многословен из-за необходимости явно указывать все поля:

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

Кстати, вы захотите использовать 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

Or:

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

Кроме того, имейте в виду, что вы можете составлять json значений без дополнительных кавычек, например если вы поместите результат json_agg в 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 на выходе.

person Craig Ringer    schedule 05.11.2012
comment
Огромное спасибо! Будет ли это примерно такая же скорость запроса, как и у оригинала, или вы считаете, что использование подзапроса требует дополнительных затрат? - person dwerner; 05.11.2012
comment
@dwerner Я был бы удивлен, если бы планы запросов не сработали так же, но настоятельно рекомендую вам explain analyze просмотреть более реалистичную выборку данных. - person Craig Ringer; 05.11.2012
comment
@dwerner Вопрос отредактирован с поясняющими ссылками, показывающими, что планы на самом деле идентичны. - person Craig Ringer; 05.11.2012
comment
Если бы я мог проголосовать за ваш ответ еще пару раз, я бы сделал это. Я ценю детали и немного о том, что 1: много отношений. - person dwerner; 05.11.2012
comment
@dwerner Рад помочь. Спасибо, что постарались написать хороший вопрос; Я бы тоже хотел поднять его еще несколько раз. Образцы данных, версия Pg, ожидаемый результат, фактический результат / ошибка; ставит галочки во всех полях, он ясен и прост для понимания. Тогда спасибо. - person Craig Ringer; 05.11.2012
comment
Есть ли недостатки в создании настраиваемого типа с желаемыми именами и приведении к этому типу (например, row_to_json(row(c1, c2, ...)::type_with_good_names)), чтобы обойти неприятный подзапрос и получить полезные имена свойств? Рассматриваемый запрос не является разовым, но это единственное, что использует настраиваемый тип. - person mu is too short; 09.09.2014
comment
@muistooshort Довольно безобидно. Всего лишь запись pg_type и еще несколько записей pg_attribute. Я бы не хотел делать это для тысяч запросов, в основном потому, что это раздражало бы в обслуживании, но это все. - person Craig Ringer; 09.09.2014
comment
@muistooshort: временная таблица для предоставления типа тоже служит и автоматически удаляется в конце сеанса. - person Erwin Brandstetter; 21.10.2014
comment
Правильно ли я говорю, что этот коммит Тома Лейна (ветки от 9.2 до HEAD) решат проблему с псевдонимами столбцов? - person vyegorov; 10.11.2014
comment
@vyegorov Я так не думаю; это всего лишь исправление, хотя и полезное. Он не добавляет поддержки для записей псевдонимов. - person Craig Ringer; 11.11.2014
comment
Большое спасибо за пример 9.4. json_build_object сделает мою жизнь намного проще, но почему-то я не уловил этого, когда увидел примечания к выпуску. Иногда для начала вам просто нужен конкретный пример. - person Jeff; 30.10.2015
comment
Супер ответ - согласитесь, что документация должна выделить json_build_object немного больше - это настоящий переломный момент. - person bobmarksie; 03.04.2017
comment
@CraigRinger Не знаете, где я могу узнать больше о scalar subqueries returning row-typed values? Я знаю, что такое скалярный подзапрос, но мне не удалось найти документацию / информацию. в части запроса, которая говорит: SELECT x FROM (...) x - person dgo.a; 24.03.2018
comment
@ dgo.a Если x - таблица, то SELECT x FROM x LIMIT 1 возвращает значение с одной строкой. Это скаляр, поскольку он возвращает одно конкретное значение. Но это значение - составной тип, строка. Вы также можете использовать конструктор строки, чтобы получить результат record, например SELECT ROW(1,2,3) - person Craig Ringer; 28.03.2018

Мое предложение по обеспечению ремонтопригодности в долгосрочной перспективе - использовать 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. *, Мне не придется обновлять эту функцию, если мне нужно обновить представление, чтобы включить больше полей в запись пользователя.

person Todd    schedule 10.02.2016
comment
Этот шагCAST (ROW (users. *) AS prominence.users) опустить, если необходимо, достаточно с именем таблицы! - person JAGJ jdfoxito; 16.02.2021

Я добавляю это решение, потому что принятый ответ не предполагает отношений 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"
          }
        ]
      }
    ]
  }
]
person Gonzalo Del cerro    schedule 18.04.2020