PostgreSQL: удалить атрибут из столбца JSON

Мне нужно удалить некоторые атрибуты из столбца типа json.

Таблица:

CREATE TABLE my_table( id VARCHAR(80), data json);
INSERT INTO my_table (id, data) VALUES (
  'A', 
  '{"attrA":1,"attrB":true,"attrC":["a", "b", "c"]}'
);

Теперь мне нужно удалить attrB из столбца data.

Что-то вроде alter table my_table drop column data->'attrB'; было бы неплохо. Но и с временным столом тоже будет достаточно.

Ответ 1

Обновить: для 9.5+ существуют явные операторы, которые вы можете использовать с jsonb (если у вас есть столбец json, вы можете использовать приведения для применения модификации):

Удаление ключа (или индекса) из объекта JSON (или из массива) может выполняться с помощью оператора -:

SELECT jsonb '{"a":1,"b":2}' - 'a', -- will yield jsonb '{"b":2}'
       jsonb '["a",1,"b",2]' - 1    -- will yield jsonb '["a","b",2]'

Удаление из глубины иерархии JSON может выполняться с помощью оператора #-:

SELECT '{"a":[null,{"b":[3.14]}]}' #- '{a,1,b,0}'
-- will yield jsonb '{"a":[null,{"b":[]}]}'

Для 9.4 вы можете использовать модифицированную версию исходного ответа (см. ниже), но вместо агрегации строки JSON вы можете сгруппировать в объект json непосредственно с помощью json_object_agg().

Связано: другие манипуляции с JSON в PostgreSQL:

Оригинальный ответ (применим к PostgreSQL 9.3):

Если у вас есть хотя бы PostgreSQL 9.3, вы можете разделить свой объект на пары с помощью json_each() и отфильтровать ваши нежелательные поля, а затем вручную создать json. Что-то вроде:

SELECT data::text::json AS before,
       ('{' || array_to_string(array_agg(to_json(l.key) || ':' || l.value), ',') || '}')::json AS after
FROM (VALUES ('{"attrA":1,"attrB":true,"attrC":["a","b","c"]}'::json)) AS v(data),
LATERAL (SELECT * FROM json_each(data) WHERE "key" <> 'attrB') AS l
GROUP BY data::text

С 9.2 (или ниже) это невозможно.

Edit

Более удобной формой является создание функции, которая может удалить любое количество атрибутов в поле json:

Изменить 2: string_agg() дешевле, чем array_to_string(array_agg())

CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" TEXT[])
  RETURNS json
  LANGUAGE sql
  IMMUTABLE
  STRICT
AS $function$
SELECT COALESCE(
  (SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}')
   FROM json_each("json")
   WHERE "key" <> ALL ("keys_to_delete")),
  '{}'
)::json
$function$;

С помощью этой функции все, что вам нужно сделать, это запустить запрос ниже:

UPDATE my_table
SET data = json_object_delete_keys(data, 'attrB');

Ответ 2

Это стало намного проще с PostgreSQL 9.5 с использованием типа JSONB. См. Операторы JSONB, зарегистрированные здесь.

Вы можете удалить атрибут верхнего уровня с помощью оператора "-".

SELECT '{"a": {"key":"value"}, "b": 2, "c": true}'::jsonb - 'a'
// -> {"b": 2, "c": true}

Вы можете использовать это в вызове обновления для обновления существующего поля JSONB.

UPDATE my_table SET data = data - 'attrB'

Вы также можете указать имя атрибута динамически через параметр, если он используется в функции.

CREATE OR REPLACE FUNCTION delete_mytable_data_key(
    _id integer,
    _key character varying)
  RETURNS void AS
$BODY$
BEGIN
    UPDATE my_table SET
        data = data - _key
    WHERE id = _id;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;

Обратный оператор - это "||", чтобы объединить два пакета JSONB вместе. Обратите внимание, что самое правильное использование атрибута будет перезаписывать любые предыдущие.

SELECT '{"a": true, "c": true}'::jsonb || '{"a": false, "b": 2}'::jsonb 
// -> {"a": false, "b": 2, "c": true}

Ответ 3

Это уродливый взлом, но если attrB не является вашим первым ключом и появляется только один раз, вы можете сделать следующее:

UPDATE my_table SET data=REPLACE(data::text, ',"attrB":' || (doc->'attrB')::text, '')::json;

Ответ 4

Еще один удобный способ сделать это - использовать расширение hstore. Таким образом, вы можете написать еще одну удобную функцию для установки/удаления ключей в json-объект. Я сделал следующую функцию, чтобы сделать то же самое:

CREATE OR REPLACE FUNCTION remove_key(json_in json, key_name text)
 RETURNS json AS $$
 DECLARE item json;
 DECLARE fields hstore;
BEGIN
 -- Initialize the hstore with desired key being set to NULL
 fields := hstore(key_name,NULL);

 -- Parse through Input Json and push each key into hstore 
 FOR item IN  SELECT row_to_json(r.*) FROM json_each_text(json_in) AS r
 LOOP
   --RAISE NOTICE 'Parsing Item % %', item->>'key', item->>'value';
   fields := (fields::hstore || hstore(item->>'key', item->>'value'));
 END LOOP;
 --RAISE NOTICE 'Result %', hstore_to_json(fields);
 -- Remove the desired key from store
 fields := fields-key_name;

 RETURN hstore_to_json(fields);
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
STRICT;

Простым примером использования является:

SELECT remove_key(('{"Name":"My Name", "Items" :[{ "Id" : 1, "Name" : "Name 1"}, { "Id" : 2, "Name 2" : "Item2 Name"}]}')::json, 'Name');
-- Result
"{"Items": "[{ \"Id\" : 1, \"Name\" : \"Name 1\"}, { \"Id\" : 2, \"Name 2\" : \"Item2 Name\"}]"}"

У меня есть еще одна функция для выполнения операции set_key, а также следующая:

CREATE OR REPLACE FUNCTION set_key(json_in json, key_name text, key_value text)
RETURNS json AS $$
DECLARE item json;
DECLARE fields hstore;
BEGIN
 -- Initialize the hstore with desired key value
 fields := hstore(key_name,key_value);

 -- Parse through Input Json and push each key into hstore 
 FOR item IN  SELECT row_to_json(r.*) FROM json_each_text(json_in) AS r
 LOOP
   --RAISE NOTICE 'Parsing Item % %', item->>'key', item->>'value';
   fields := (fields::hstore || hstore(item->>'key', item->>'value'));
 END LOOP;
 --RAISE NOTICE 'Result %', hstore_to_json(fields);
 RETURN hstore_to_json(fields);
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
STRICT;

Я уже обсуждал это в своем блоге здесь.

Ответ 5

Я не мог получить SELECT '{"a": "b"}'::jsonb - 'a'; для работы в 9.5.2. Однако SELECT '{"a": "b"}'::jsonb #- '{a}'; действительно работает!