PostgreSQL: обновление атрибутов элементов во вложенных массивах в структуре JSONB - PullRequest
1 голос
/ 15 апреля 2019

У меня есть структура jsonb в PostgreSQL 9.6, которая содержит структуру вложенного массива, подобную примеру ниже:

continents:[
   {
       id: 1,
       name: 'North America',
       countries: [
           {
               id: 1,
               name: 'USA',
               subdivision: [
                  {
                     id: 1,
                     name: 'Oregon',
                     type: 'SOME_TYPE'
                  }
               ]
           } 
       ]
   }
]

Как я могу изменить атрибут 'type' нескольких подразделений, если он вложен в два массива ( страны и подразделения )?

Я сталкивался с другими ответами и могу делать это для каждой записи отдельно (при условии, что таблица map , а столбец jsonb Divisions ):

update map
set divisions = jsonb_set( divisions, '{continents,0,countries,0,subdivisions,0,type}', '"STATE"', FALSE);

Есть ли способ программно изменить этот атрибут для всех подразделений?

Я думаю, что я уже близко, я могу запросить все типы подразделов, используя запрос ниже, но изо всех сил пытаюсь выяснить, как их обновить:

WITH subdivision_data AS (
    WITH country_data AS (
       select continents -> 'countries' as countries
       from  map, jsonb_array_elements( map.divisions -> 'continents' ) continents
    )
    select country_item -> 'subdivisions' as subdivisions
    from country_data cd, jsonb_array_elements( cd.countries ) country_item
)
select subdivision_item ->> 'type' as subdivision_type
from subdivision_data sub, jsonb_array_elements( sub.subdivisions ) subdivision_item;

Вот некоторые из вопросов, с которыми я столкнулся. Похоже, они работают, только если вы пытаетесь обновить одноуровневый массив, хотя:

postgresql 9.5 с использованием jsonb_set для обновления определенного значения массива jsonb

Как обновить глубоко вложенный объект JSON на основе критериев фильтрации в Postgres?

Postgres / JSON - обновить все элементы массива

Ответы [ 2 ]

0 голосов
/ 20 апреля 2019

Сначала я подумал, что что-то вроде этого будет работать:

update map as m set
    divisions = jsonb_set(m1.divisions, array['continents',(d.rn-1)::text,'countries',(c.rn-1)::text,'subdivisions',(s.rn-1)::text,'type'], '"STATE"', FALSE)
from map as m1,
    jsonb_array_elements(m1.divisions -> 'continents') with ordinality as d(data,rn),
    jsonb_array_elements(d.data -> 'countries') with ordinality as c(data,rn),
    jsonb_array_elements(c.data -> 'subdivisions') with ordinality as s(data,rn)
where
    m1.id = m.id

db<>fiddle demo

Но это не работает - см. documentation

Когда присутствует предложение FROM, то, по сути, происходит то, что таблица назначения объединяется с таблицами, упомянутыми в списке from_list, и каждая выходная строка объединения представляет операцию обновления для целевой стол. При использовании FROM вы должны убедиться, что соединение производит не более одной выходной строки для каждой строки, подлежащей изменению. Другими словами, Целевая строка не должна соединяться более чем с одной строкой из другой стол (ы). Если это так, то только одна из строк соединения будет использоваться для обновить целевую строку, но какая из них будет использоваться не сразу предсказуемы.

То, что вы можете сделать, это разложить ваши jsons с помощью functions-json и затем собрать их обратно:

update map set
    divisions = jsonb_set(divisions, array['continents'],
        (select
            jsonb_agg(jsonb_set(
                d, array['countries'],
                (select 
                    jsonb_agg(jsonb_set(
                        c, array['subdivisions'],
                        (select
                            jsonb_agg(jsonb_set(s, array['type'], '"STATE"', FALSE))
                        from jsonb_array_elements(c -> 'subdivisions') as s),
                        FALSE
                    ))
                from jsonb_array_elements(d -> 'countries') as c)
            ))
        from jsonb_array_elements(divisions -> 'continents') as d),
        FALSE
    )

db<>fiddle demo

Вы также можете создать вспомогательную функцию, которую вы можете использовать вместо нескольких подзапросов:

create function jsonb_update_path(_data jsonb, _path text[], _value jsonb)
returns jsonb
as $$
begin
    if array_length(_path, 1) = 1 then
        return jsonb_set(_data, _path, _value, FALSE);
    else
        return (
            jsonb_set(
                _data, _path[1:1],
                (
                    select
                        jsonb_agg(jsonb_update_path(e, _path[2:], _value))
                    from jsonb_array_elements(_data -> _path[1]) as e
                )
            )
        );
    end if;
end
$$
language plpgsql

update map set
    divisions = jsonb_update_path(divisions, '{continents,countries,subdivisions,type}', '"STATE"')

db<>fiddle demo

0 голосов
/ 16 апреля 2019

1 Общий способ сделать это - взорвать json, заменить значения, используя обычный старый sql, и агрегировать обратно в исходную форму json.Но это требует от вас полного знания структуры документа

Вот пример этого в отдельном операторе выбора

WITH data(map) AS (
VALUES(JSONB '{"continents":[{"id": 1,"name": "North America","countries": [{"id": 1,"name": "USA","subdivision": [{"id": 1,"name": "Oregon","type": "SOME_TYPE"}]}]}]}')
)
, expanded AS (
SELECT 
  (continents#>>'{id}')::int continent_id
, continents#>>'{name}' continent_name 
, (countries#>>'{id}')::int country_id
, countries#>>'{name}' country_name
, (subdivisions#>>'{id}')::int subdivision_id
, subdivisions#>>'{name}' subdivision_name
, CASE WHEN subdivisions#>>'{type}' = 'SOME_TYPE'      -- put all update where conditions here
        AND continents#>>'{name}' = 'North America'    -- this is where the value is changed
  THEN 'POTATO' 
  ELSE subdivisions#>>'{type}' 
  END subdivision_type
FROM data
, JSONB_ARRAY_ELEMENTS(map#>'{continents}') continents
, JSONB_ARRAY_ELEMENTS(continents#>'{countries}') countries
, JSONB_ARRAY_ELEMENTS(countries#>'{subdivision}') subdivisions
)
, subdivisions AS (
SELECT continent_id
, continent_name
, country_id
, country_name
, JSONB_BUILD_OBJECT('subdivisions', JSONB_AGG(JSONB_BUILD_OBJECT('id', subdivision_id, 'name', subdivision_name, 'type', subdivision_type))) subdivisions
FROM expanded
GROUP By 1, 2, 3, 4
)
, countries AS (
SELECT
  continent_id
, continent_name
, JSONB_BUILD_OBJECT('countries', JSONB_AGG(JSONB_BUILD_OBJECT('id', country_id, 'name', country_name, 'subdivision', subdivisions))) countries
FROM subdivisions
GROUP BY 1, 2
)
SELECT JSONB_BUILD_OBJECT('continents', JSONB_AGG(JSONB_BUILD_OBJECT('id', continent_id, 'name', continent_name, 'countries', countries))) map
FROM countries

Поместив это в запрос на обновление, мы получим следующеегде я предполагаю, что исходная таблица называется data, и у нее есть уникальный столбец с именем id

UPDATE data SET map = updated.map
FROM (
expanded AS (
SELECT data.id data_id 
, (continents#>>'{id}')::int continent_id
, continents#>>'{name}' continent_name 
, (countries#>>'{id}')::int country_id
, countries#>>'{name}' country_name
, (subdivisions#>>'{id}')::int subdivision_id
, subdivisions#>>'{name}' subdivision_name
, CASE WHEN subdivisions#>>'{type}' = 'SOME_TYPE' 
        AND continents#>>'{name}' = 'North America' 
  THEN 'POTATO' 
  ELSE subdivisions#>>'{type}' 
  END subdivision_type
FROM data
, JSONB_ARRAY_ELEMENTS(map#>'{continents}') continents
, JSONB_ARRAY_ELEMENTS(continents#>'{countries}') countries
, JSONB_ARRAY_ELEMENTS(countries#>'{subdivision}') subdivisions
)
, subdivisions AS (
SELECT
  data_id
, continent_id
, continent_name
, country_id
, country_name
, JSONB_BUILD_OBJECT('subdivisions', JSONB_AGG(JSONB_BUILD_OBJECT('id', subdivision_id, 'name', subdivision_name, 'type', subdivision_type))) subdivisions
FROM expanded
GROUP By 1, 2, 3, 4, 5
)
, countries AS (
SELECT
  data_id
, continent_id
, continent_name
, JSONB_BUILD_OBJECT('countries', JSONB_AGG(JSONB_BUILD_OBJECT('id', country_id, 'name', country_name, 'subdivision', subdivisions))) countries
FROM subdivisions
GROUP BY 1, 2, 3
)
SELECT data_id, JSONB_BUILD_OBJECT('continents', JSONB_AGG(JSONB_BUILD_OBJECT('id', continent_id, 'name', continent_name, 'countries', countries))) map
FROM countries
GROUP BY 1

) updated
WHERE updated.data_id = data.id
...