Объединение значений JSONB в PostgreSQL?

Использование оператора || дает следующий результат:

select '{"a":{"b":2}}'::jsonb || '{"a":{"c":3}}'::jsonb ;
    ?column?     
-----------------
 {"a": {"c": 3}}
(1 row)

Я хотел бы добиться следующего результата (?? просто заполнитель для оператора):

select '{"a":{"b":2}}'::jsonb ?? '{"a":{"c":3}}'::jsonb ;
    ?column?     
-----------------
 {"a": {"b": 2, "c": 3}}
(1 row)

Итак, вы можете видеть, что у ключа верхнего уровня a дочерние значения «объединены», так что результат содержит как b, так и c.

Как выполнить «глубокое» слияние двух значений JSONB в Postgres?

Возможно ли это, если да, то как?


Более сложный тестовый пример:

select '{"a":{"b":{"c":3},"z":true}}'::jsonb ?? '{"a":{"b":{"d":4},"z":false}}'::jsonb ;
    ?column?     
-----------------
 {"a": {"b": {"c": 3, "d": 4}, "z": false}}
(1 row)

Еще один тестовый пример, когда примитив «сливается» и объект:

select '{"a":{"b":{"c":3},"z":true}}'::jsonb ?? '{"a":{"b":false,"z":false}}'::jsonb ;
        ?column?         
-----------------
 {"a": {"b": false, "z": false}}
(1 row)

person bguiz    schedule 22.03.2017    source источник
comment
Это может помочь: dba.stackexchange.com/q/166092/1822   -  person a_horse_with_no_name    schedule 22.03.2017


Ответы (3)


Вы должны объединить невложенные элементы, используя jsonb_each() для обоих значений. Выполнение этого в нетривиальном запросе может быть неудобным, поэтому я бы предпочел настраиваемую функцию, подобную этой:

create or replace function jsonb_my_merge(a jsonb, b jsonb)
returns jsonb language sql as $$
    select 
        jsonb_object_agg(
            coalesce(ka, kb), 
            case 
                when va isnull then vb 
                when vb isnull then va 
                else va || vb 
            end
        )
    from jsonb_each(a) e1(ka, va)
    full join jsonb_each(b) e2(kb, vb) on ka = kb
$$;

Использовать:

select jsonb_my_merge(
    '{"a":{"b":2}, "d": {"e": 10}, "x": 1}'::jsonb, 
    '{"a":{"c":3}, "d": {"f": 11}, "y": 2}'::jsonb
)

                          jsonb_my_merge                          
------------------------------------------------------------------
 {"a": {"b": 2, "c": 3}, "d": {"e": 10, "f": 11}, "x": 1, "y": 2}
(1 row)

Вы можете немного изменить функцию с помощью рекурсии, чтобы получить решение, работающее на любом уровне вложенности:

create or replace function jsonb_recursive_merge(a jsonb, b jsonb)
returns jsonb language sql as $$
    select 
        jsonb_object_agg(
            coalesce(ka, kb), 
            case 
                when va isnull then vb 
                when vb isnull then va 
                when jsonb_typeof(va) <> 'object' then va || vb
                else jsonb_recursive_merge(va, vb)
            end
        )
    from jsonb_each(a) e1(ka, va)
    full join jsonb_each(b) e2(kb, vb) on ka = kb
$$;

Примеры:

select jsonb_recursive_merge( 
    '{"a":{"b":{"c":3},"x":5}}'::jsonb, 
    '{"a":{"b":{"d":4},"y":6}}'::jsonb);

             jsonb_recursive_merge              
------------------------------------------------
 {"a": {"b": {"c": 3, "d": 4}, "x": 5, "y": 6}}
(1 row)

select jsonb_recursive_merge(
    '{"a":{"b":{"c":{"d":{"e":1}}}}}'::jsonb, 
    '{"a":{"b":{"c":{"d":{"f":2}}}}}'::jsonb)

            jsonb_recursive_merge             
----------------------------------------------
 {"a": {"b": {"c": {"d": {"e": 1, "f": 2}}}}}
(1 row)

Наконец, вариант функции с изменениями, предложенный OP (см. Комментарии ниже):

create or replace function jsonb_recursive_merge(a jsonb, b jsonb) 
returns jsonb language sql as $$ 
select 
    jsonb_object_agg(
        coalesce(ka, kb), 
        case 
            when va isnull then vb 
            when vb isnull then va 
            when jsonb_typeof(va) <> 'object' or jsonb_typeof(vb) <> 'object' then vb 
            else jsonb_recursive_merge(va, vb) end 
        ) 
    from jsonb_each(a) e1(ka, va) 
    full join jsonb_each(b) e2(kb, vb) on ka = kb 
$$;
person klin    schedule 22.03.2017
comment
Я пробовал эту функцию, используя более сложный объект: select jsonb_my_merge( '{"a":{"b":{"c":3},"x":5}}'::jsonb, '{"a":{"b":{"d":4},"y":6}}'::jsonb );, и результат, который он вернул, был {"a": {"b": {"d": 4}, "x": 5, "y": 6}} то, что я хотел бы вернуть, это {"a": {"b": {"c":3, "d": 4}, "x": 5, "y": 6}}. - person bguiz; 23.03.2017
comment
... на самом деле, я только что столкнулся со сценарием, когда это ломается: select jsonb_recursive_merge( '{"a":{"b":{"c":3},"z":true}}'::jsonb, '{"a":{"b":{"d":4},"z":false}}'::jsonb); дает {"a": {"b": {"c": 3, "d": 4}, "z": [true, false]}} ... но я ожидал, что он не создаст этот массив, а вместо этого даст следующее: {"a": {"b": {"c": 3, "d": 4}, "z": false}} - person bguiz; 23.03.2017
comment
create or replace function jsonb_recursive_merge(a jsonb, b jsonb) returns jsonb language sql as $$ select jsonb_object_agg( coalesce(ka, kb), case when va isnull then vb when vb isnull then va when jsonb_typeof(va) <> 'object' then vb else jsonb_recursive_merge(va, vb) end ) from jsonb_each(a) e1(ka, va) full join jsonb_each(b) e2(kb, vb) on ka = kb $$; Что вы думаете об этом? То же самое, за исключением случаев, когда тип не равен объекту then va || vb становится then vb - person bguiz; 23.03.2017
comment
Я думаю, это нормально. Вы можете изменить функцию в соответствии со своими ожиданиями. Ваш вариант кажется вполне логичным, может быть, более оригинальным. - person klin; 23.03.2017
comment
Кстати, я нашел еще один крайний случай, когда объект объединяется примитивом (см. Обновление qn) create or replace function jsonb_recursive_merge(a jsonb, b jsonb) returns jsonb language sql as $$ select jsonb_object_agg( coalesce(ka, kb), case when va isnull then vb when vb isnull then va when (jsonb_typeof(va) <> 'object' or jsonb_typeof(vb) <> 'object') then vb else jsonb_recursive_merge(va, vb) end ) from jsonb_each(a) e1(ka, va) full join jsonb_each(b) e2(kb, vb) on ka = kb $$; Незначительные изменения, необходимые для функции ^ - person bguiz; 24.03.2017
comment
Да, условие должно быть симметричным из-за full join. Я добавил ваш вариант к ответу. - person klin; 24.03.2017

Такое «глубокое слияние» можно интерпретировать по-разному, в зависимости от вашего варианта использования. Для полноты картины моя интуиция обычно диктует следующие правила:

  • объект + объект: каждое свойство сохраняется от каждого объекта, которого нет в другом объекте (значение JSON null считается в объекте, если оно явно указано). Когда свойство присутствует в обоих объектах, слияние продолжается рекурсивно с теми же правилами (этот момент обычно согласовывается).
  • массив + массив: результатом является объединение двух массивов.
  • массив + примитив / объект: результат - первый массив с добавленным к нему вторым значением JSON.
  • любые другие случаи: результатом является второе значение JSON (например, примитивы или несовместимые типы переопределяют друг друга).

create or replace function jsonb_merge_deep(jsonb, jsonb)
  returns jsonb
  language sql
  immutable
as $func$
  select case jsonb_typeof($1)
    when 'object' then case jsonb_typeof($2)
      when 'object' then (
        select    jsonb_object_agg(k, case
                    when e2.v is null then e1.v
                    when e1.v is null then e2.v
                    else jsonb_merge_deep(e1.v, e2.v)
                  end)
        from      jsonb_each($1) e1(k, v)
        full join jsonb_each($2) e2(k, v) using (k)
      )
      else $2
    end
    when 'array' then $1 || $2
    else $2
  end
$func$;

Дополнительным преимуществом этой функции является то, что ее можно вызывать буквально с любым типом значений JSON: всегда дает результат и никогда не жалуется на типы значений JSON.

http://rextester.com/FAC95623

person pozs    schedule 24.03.2017

После PostgreSQL 9.5 вы можете использовать функцию jsonb_set:

  1. '{a,c}' смотрит в путь, если его нет, он будет создан.
  2. '{"a":{"c":3}}'::jsonb#>'{a,c}' это получит значение c

new_value добавлено, если create_missing истинно (по умолчанию истинно)

Это документ jsonb -functions

select jsonb_set('{"a":{"b":2}}', '{a,c}','{"a":{"c":3}}'::jsonb#>'{a,c}' )

Result:  {"a":{"c":3,"b":2}}

Слить сразу несколько атрибутов:

with jsonb_paths(main_part,missing_part) as (
values ('{"a":{"b":2}}','{"a":{"c":3,"d":4}}')
)
select jsonb_object_agg(t.k,t.v||t2.v)
from jsonb_paths,
jsonb_each(main_part::jsonb) t(k,v),
jsonb_each(missing_part::jsonb) t2(k,v);

result: {"a":{"c":3,"b":2,"d":4}}
person light souls    schedule 22.03.2017
comment
Интересно, что я не думал об использовании функции jsonb_set. H / w это означает, что мне нужно (а) знать / указать путь, который необходимо объединить, и (б) можно объединять только один атрибут за раз. - person bguiz; 22.03.2017
comment
Пробовал этот второй запрос, который вы добавили с более сложными значениями: with jsonb_paths(main_part,missing_part) as ( values ('{"a":{"b":{"c":3},"x":5}}','{"a":{"b":{"d":4},"y":6}}') ) select jsonb_object_agg(t.k,t.v||t2.v) from jsonb_paths, jsonb_each(main_part::jsonb) t(k,v), jsonb_each(missing_part::jsonb) t2(k,v); результат был {"a": {"b": {"d": 4}, "x": 5, "y": 6}} результат, который я хотел бы, {"a": {"b": {"c":3, "d": 4}, "x": 5, "y": 6}} ... так что похоже, что он обрабатывает только один уровень глубины. - person bguiz; 23.03.2017
comment
Правильно, jsonb_set не сработает, если не существует одного или нескольких родителей. Я думаю, что он создаст ключ только в том случае, если родительский объект является объектом, а ключ еще не существует. - person piojo; 15.03.2018