postgresql Postgres从嵌套的jsonb数组中删除jsonb对象

vuv7lop3  于 2024-01-07  发布在  PostgreSQL
关注(0)|答案(2)|浏览(148)

我有一个嵌套很深的json数组。数组包含一个json对象列表,我想删除任何基于对象内字段匹配的对象。

{
  "name": "John smith",
  "items": {
    "unknown-key-1": {
      "file-array": [
        {
          "file-id": "file-1"
        },
        {
          "file-id": "file-2"
        },
        {
          "file-id": "file-3"
        }
      ]
    },
    "unknown-key-2": {
      "file-array": [
        {
          "file-id": "file-1"
        },
        {
          "file-id": "file-2"
        }
      ]
    }
  }
}

字符串
例如,假设我想删除所有file-id为“file-1”的对象。我的UPDATE语句将删除所有与file-id匹配的jsonb对象。在UPDATE之后,我的jsonb列将如下所示:

{
  "name": "John smith",
  "items": {
    "unknown-key-1": {
      "file-array": [
        {
          "file-id": "file-2"
        },
        {
          "file-id": "file-3"
        }
      ]
    },
    "unknown-key-2": {
      "file-array": [
        {
          "file-id": "file-2"
        }
      ]
    }
  }
}


当数组位于json的顶层时,或者当数组嵌套在对象中并且键已知时,我可以实现这一点。但在这种情况下,键是动态生成的(即“unknown-key-1”,“unknown-key-2”)
我知道在适当的情况下,我会将数据规范化,因为这是一个反模式,但是我别无选择。而且我想使用UPDATE语句而不是Postgres函数来实现这一点。

nfeuvbwi

nfeuvbwi1#

修改jsonb并不像应用它作为更新那样棘手:

select jdata #- array['items',item,'file-array',file_index-1]::text[]
from test
    ,jsonb_each(jdata->'items')a(item,contents)
    ,jsonb_array_elements(contents->'file-array')with ordinality b(file_obj,file_index)
where (file_obj->>'file-id')='file-1';

字符串
对于每一行,它列出了它的items,对于每一行,它列出了file-array下的内容。这产生了可以在where中过滤的项目名称和文件索引,并用于使用#-减去路径。从with ordinality产生的索引是基于1的,而jsonb数组是基于0的,因此是-1
直接将其用作更新批处理的问题是,它会导致基于每个文件的单个、单独的原子更改,这些更改将从每个值中删除,并且每行仅应用其中一个小更改。在您的示例中,只有unknown-key-2 * 或 * unknown-key-1下的文件会删除file-1,而不是两者都删除。请参阅update文档下的注解:
当使用FROM时,您应该确保连接为每个要修改的行最多生成一个输出行。换句话说,目标行不应该连接到其他表中的多个行。如果是这样,则只有一个连接行将用于更新目标行,但将使用哪一个并不容易预测。
您必须继续运行相同的更新,直到不再看到受影响的行,或者您需要压缩更新,以便每行只应用一个累积更改:demo at db<>fiddle

with recursive required_changes as (
    select id,
      jdata,
      item,
      file_index-1 as file_index,
      row_number()over(partition by id) as update_round
    from test
        ,jsonb_each(jdata->'items')a(item,contents)
        ,jsonb_array_elements(contents->'file-array')
         with ordinality b(file_obj,file_index)
    where (file_obj->>'file-id')='file-1')
,iteratively_merged_changes as (
    select id,
         jdata#-array['items',item,'file-array',file_index]::text[] jdata,
         2 as next_round
    from required_changes where update_round=1
    union
    select a.id,
         a.jdata#-array['items',item,'file-array',file_index]::text[],
         a.next_round+1
    from iteratively_merged_changes a join required_changes b 
    on a.id=b.id 
    and b.update_round=a.next_round)
,final_batch as (
    select distinct on(id)id,jdata 
    from iteratively_merged_changes order by id,next_round desc)
update test t set jdata=f.jdata
from final_batch f where t.id=f.id
returning t.*;


第一个CTE找到需要从哪一行删除的路径,第二个CTE不断迭代地应用这些更改,一个在另一个之上,第三个CTE distinct on只是将最新一轮的最终值传递给外部update
WITH需要是RECURSIVE,第二个才能自引用,但只有那个才能自引用。More tests

jljoyd4f

jljoyd4f2#

当你确切地知道你需要在整个JSON对象中查找哪些特定的键值,以及目标对象的一般结构和位置时,@ Zeberk的答案是有用的。然而,在一般情况下,你需要能够处理任何键值和任何对象结构和组合。下面的查询首先找到JSON中每个项目的路径,然后使用这些路径来搜索要删除的目标对象

with recursive cte(id, obj, arr, path) as (
   -- get all paths to every item in the object, regardless of key name and structure
   select t.id, case when jsonb_typeof(t.js) = 'object' then t.js else '{"null":null}'::jsonb end, 
      case when jsonb_typeof(t.js) = 'array' then t.js else '[null]'::jsonb end, ''
   from tbl t
   union all
   select c.id, case when jsonb_typeof(p_obj.value) = 'object' then p_obj.value 
      when jsonb_typeof(p_arr.value) = 'object' then p_arr.value else 
         (case when jsonb_typeof(p_obj.value) in ('array', 'object') or  
             jsonb_typeof(p_arr.value) in ('array', 'object') then '{"null":null}'::jsonb else '{}'::jsonb end) end,
      case when jsonb_typeof(p_obj.value) = 'array' then p_obj.value 
        when jsonb_typeof(p_arr.value) = 'array' then p_arr.value else 
         (case when jsonb_typeof(p_obj.value) in ('array', 'object') or  
             jsonb_typeof(p_arr.value) in ('array', 'object') then '[null]'::jsonb else '[]'::jsonb end) end,
      c.path || case when c.path != '' then ',' else '' end || case when p_arr.value#>>'{}' is not null then cast(p_arr.r as text) else p_obj.key end
   from cte c cross join jsonb_each(c.obj) p_obj cross join lateral (
      select row_number() over (order by 1) - 1 r, k.value 
      from jsonb_array_elements(c.arr) k) p_arr 
),
paths as (
   -- find the paths generated in the cte above that contain a key 'file-id' with a corresponding value of 'file-1' 
   select c.id, ('{'||c.path||'}')::text[] path from cte c join tbl t on t.id = c.id 
   where ((t.js#>('{'||c.path||'}')::text[]) -> 'file-id')#>>'{}' = 'file-1' 
),
load_update as (
   select row_number() over (partition by p.id) r, p.id, t.js, p.path from paths p join tbl t on p.id = t.id
),
perform_update as (
   -- make the updates in-place
   select u.r, u.id, u.js #- u.path js from load_update u where u.r = 1
   union all
   select u.r, u.id, p.js #- u.path js 
   from perform_update p join load_update u on u.r = p.r + 1 and p.id = u.id
)
update tbl set js = p.js from perform_update p 
where tbl.id = p.id and p.r = (select max(p1.r) from load_update p1 where p1.id = p.id)

字符串
See fiddle

相关问题