简体   繁体   中英

Postgresql update jsonb keys recursively

Having the following datamodel:

create table test
(
    id int primary key,
    js jsonb
);
insert into test values (1, '{"id": "total", "price": 400, "breakdown": [{"id": "product1", "price": 400}] }');
insert into test values (2, '{"id": "total", "price": 1000, "breakdown": [{"id": "product1", "price": 400}, {"id": "product2", "price": 600}]}');

I need to update all the price keys to a new name cost . It is easy to do that on the static field, using:

update test
set js = jsonb_set(js #- '{price}', '{cost}', js #> '{price}');

result:

1 {"id": "total", "cost": 1000, "breakdown": [{"id": "product1", "price": 400}]}
2 {"id": "total", "cost": 2000, "breakdown": [{"id": "product1", "price": 400}, {"id": "product2", "price": 600}]}

But I also need to do this inside the breakdown array.

How can I do this without knowing the number of items in the breakdown array? In other words, how can I apply a function in place on every element from a jsonb array.

Thank you!

First you create an aggregate function simlilar to jsonb_set :

CREATE OR REPLACE FUNCTION jsonb_set(x jsonb, y jsonb, _path text[], _key text, _val jsonb, create_missing boolean DEFAULT True)
RETURNS jsonb LANGUAGE sql IMMUTABLE AS
$$
    SELECT jsonb_set(COALESCE(x, y), COALESCE(_path, '{}' :: text[]) || _key, COALESCE(_val, 'null' :: jsonb), create_missing) ;
$$ ;

DROP AGGREGATE IF EXISTS jsonb_set_agg (jsonb, text[], text, jsonb, boolean) CASCADE ;
CREATE AGGREGATE jsonb_set_agg (jsonb, text[], text, jsonb, boolean)
(
  sfunc = jsonb_set
, stype = jsonb
) ;

Then, you call the aggregate function while iterating on the jsonb array elements :

WITH list AS (
SELECT id, jsonb_set_agg(js #- '{breakdown,' || ind || ',price}', '{breakdown,' || ind || ',cost}', js #> '{breakdown,' || ind || ',price}', true) AS js
FROM table
CROSS JOIN LATERAL generate_series(0, jsonb_array_length(js->'{breakdown}') - 1) AS ind
GROUP BY id)
UPDATE table AS t
SET js = l.js
FROM list AS l
WHERE t.id = l.id ;

This query is sample and easy for change field:

You can see my query structure in: dbfiddle

update test u_t
set js = tmp.new_js
from (
         select t.id,
                (t.js || jsonb_build_object('cost', t.js ->> 'price')) - 'price'
                    ||
                jsonb_build_object('breakdown', jsonb_agg(
                        (b.value || jsonb_build_object('cost', b.value ->> 'price')) - 'price')) as new_js
         from test t
                  cross join jsonb_array_elements(t.js -> 'breakdown') b
         group by t.id) tmp
where u_t.id = tmp.id;

Another way to replace jsonb key in all jsonb objets into a jsonb array:

My query disaggregate the jsonb array. For each object, if price key exist, remove the price key from jsonb object, add the new cost key with the old price 's value, then create a new jsonb array with the modified objects. Finally replace the old jsonb array with the new one.

WITH cte AS (SELECT id, jsonb_agg(CASE WHEN item ? 'price' 
                                       THEN jsonb_set(item - 'price', '{"cost"}', item -> 'price')
                                       ELSE item END) AS cost_array
              FROM test
              CROSS JOIN jsonb_array_elements(js -> 'breakdown') WITH ORDINALITY arr(item, index)
              GROUP BY id)
              
UPDATE test
SET js = jsonb_set(js, '{breakdown}', cte.cost_array, false)
FROM cte
WHERE cte.id = test.id;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM