PostgreSQL:更新JSONB结构中嵌套数组中元素的属性

问题描述 投票:1回答:2

我在PostgreSQL 9.6中有一个jsonb结构,它包含一个类似于下面例子的嵌套数组结构:

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

如何更改多个细分的'type'属性,因为它嵌套在两个数组(国家和细分)中?

我已经遇到了其他答案,并且能够按记录逐个记录(假设表格是地图而jsonb列是分区):

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 using jsonb_set for updating specific jsonb array value

How to update deeply nested JSON object based on filter criteria in Postgres?

Postgres/JSON - update all array elements

arrays json postgresql jsonb postgresql-json
2个回答
0
投票

1这样做的一般方法是爆炸json,使用普通的旧sql替换值并聚合回原始的json形状。但这要求您完全了解文档结构

以下是自包含select语句中的示例

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

0
投票

起初我认为像这样的东西会起作用:

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时,应确保连接为每个要修改的行生成最多一个输出行。换句话说,目标行不应该连接到其他表的多个行。如果是,那么只有一个连接行将用于更新目标行,但是将使用哪一个不容易预测。

你可以做的是用functions-json取消你的jsons,然后将它们汇总回来:

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

© www.soinside.com 2019 - 2024. All rights reserved.