PostgreSQL:更新JSONB结构中嵌套数组中元素的属性
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PostgreSQL:更新JSONB结构中嵌套数组中元素的属性相关的知识,希望对你有一定的参考价值。
我在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?
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
起初我认为像这样的东西会起作用:
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
但这不起作用 - 见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
)
您还可以创建辅助函数,而不是使用多个子查询:
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"')
以上是关于PostgreSQL:更新JSONB结构中嵌套数组中元素的属性的主要内容,如果未能解决你的问题,请参考以下文章
plpgsql jsonb_set 用于带有嵌套数组的 JSON 对象数组
在 postgresql、选项数组或对象中插入 jsonb 数据,有效方式