# Postgres JSONB: Updating data inside an array of objects
Recently I had the challenge to update data inside of an array of objects within a JSONB column.
Given the following two tables
CREATE TABLE contract (
id BIGINT PRIMARY KEY,
json_data JSONB NOT NULL
);
CREATE TABLE invoice (
id BIGINT PRIMARY KEY,
json_data JSONB NOT NULL
);
In this example an invoice has different items and each item can belong to a different contract. Let's have a look at some example data.
-- contract
{
"id": 1,
"customerId": 123
}
-- invoice
{
"id": 15,
"items": [
{
"contractId": 1,
"prize": 123
},
{
"contractId": 2,
"prize": 456
}
]
}
What we wanted to do is add the customer id, based on the contract id, to the items.
Postgres offers a jsonb_set
function for updating JSON fields.
jsonb_set(target jsonb, path text[], new_value jsonb [, create_missing boolean])
The second parameter path
defines, which property you want to update.
To update items in an array, you can use an index-based approach.
To update the first entry in the items array in the example above, a path woud look like this: {items, 0, customerId}
.
This sets the field "customerId" at index 0 (first entry) in the array named items.
Let's have a look at the update query before breaking it down.
WITH item AS (
SELECT ('{items,' || index - 1 || ',"customerId"}')::TEXT[] AS path,
id,
(SELECT (json_data ->> 'customerId')::BIGINT FROM contract where id = (item ->> 'contractId')::BIGINT ) AS customerId
FROM invoice,
jsonb_array_elements(json_data -> 'items') WITH ORDINALITY arr(item, index)
WHERE item ->> 'contractId' IS NOT NULL
)
UPDATE invoice i
SET json_data = jsonb_set(json_data, item.path, to_json(item.customerId)::JSONB)
FROM item
WHERE i.id = item.id;
# Breaking it down
-- The basic value of SELECT in WITH is to break down complicated queries into simpler parts.
WITH item AS (
SELECT
-- dynamically concat the JSONB path. {items,<index>,customerId} (sets the field custmerId in the array named items at the index number), aliased as path for easier reference
('{items,' || index - 1 || ',"customerId"}')::TEXT[] AS path,
-- We need the id for updating the correct entry later
id,
-- Subquery to select the customerId based on the contract id, aliased as customerId for easier reference
(SELECT (json_data ->> 'customerId')::BIGINT FROM contract where id = (item ->> 'contractId')::BIGINT ) AS customerId
-- source table
FROM invoice,
-- flatten the array using jsonb_array_elements and allow access for both, the item itself (so you can access the properties of the item object) and the index using WITH ORDINALITY
jsonb_array_elements(json_data -> 'items') WITH ORDINALITY arr(item, index)
)
-- Now we actually start updating the invoices
UPDATE invoice i
-- item is the name of the with-subquery above, we can now access the dynamically built path and selected customer id
SET json_data = jsonb_set(json_data, item.path, to_json(item.customerId)::JSONB)
-- reference the with-query above
FROM item
WHERE i.id = item.id;