SQL Server:从另一个json更新json–;一般地

SQL Server:从另一个json更新json–;一般地,json,sql-server,sql-server-json,Json,Sql Server,Sql Server Json,在SQL Server中,如何在不显式使用键/定义列的情况下更新/合并一个json与另一个json 一些背景:我将元数据作为json存储在varchar(max)列中。在同一个表中,每个记录可以有不同的元数据键。比如将人员和产品存储在同一张表中。类似于但不是值表,我使用json列将元数据存储为键值对。 这就是为什么我在寻找一个通用的解决方案 i、 e.一条记录可以有元数据 {"last_name":"John","first_name":"Smith","age":28,"Address":"1

在SQL Server中,如何在不显式使用键/定义列的情况下更新/合并一个json与另一个json

一些背景:我将元数据作为json存储在
varchar(max)
列中。在同一个表中,每个记录可以有不同的元数据键。比如将人员和产品存储在同一张表中。类似于但不是值表,我使用json列将元数据存储为键值对。 这就是为什么我在寻找一个通用的解决方案

i、 e.一条记录可以有元数据

{"last_name":"John","first_name":"Smith","age":28,"Address":"123 Steels st…"}
{"product_name":"Box","material":"plastic","Price":1.5,"Weight":20,"Height":15}
同一表中的另一条记录可以有元数据

{"last_name":"John","first_name":"Smith","age":28,"Address":"123 Steels st…"}
{"product_name":"Box","material":"plastic","Price":1.5,"Weight":20,"Height":15}
我正在寻找一种高效/现代的方法,从json更新/添加json中的多个值

i、 e.来源

{
    "last_name": "John",
    "first_name": "Smith",
    "age": 28,
    "weight":79
    "address": "123 Steels st…"
}
要更新/添加的内容:

{   
    "address": "567 Yonge Ave…"
    "last_name": "Johnny"
    "age": 35
    "height":1.83
}
结果-来源更新为:

{
    "last_name":"Smith",
    "first_name": "Johnny",         - updated
    "age": 35,                      - updated
    "weight":79
    "address": "567 Yonge Ave…"     - updated
    "height":1.83                   - added
}
我的解决方案:

declare @j_source varchar(200) = '{"first_name": "Smith", "last_name": "Smith","age": 28,"weight":79,"address": "123 Steels st…"}'
declare @j_update varchar(200) = '{"address": "567 Yonge Ave…","first_name": "Johnny","age": 35, "height":1.83}'

print @j_source
print @j_update

-- transform json to tables
select *
into #t_source
from openjson(@j_source)

select *
into #t_update
from openjson(@j_update)

-- combine the updated values with new values with non-updated values
select *
into #t_result
from
(
    -- get key values that are not being updated
    select ts.[key],ts.[value],ts.[type] 
    from #t_source as ts
    left join #t_update as tu
    on ts.[key] = tu.[key]
    where tu.[key] is null

    union -- get key values that are being updated. side note: the first and second select can be combined into one using isnull

    select ts.[key],tu.[value],ts.[type] -- take value from #t_update
    from #t_source as ts
    inner join #t_update as tu
    on ts.[key] = tu.[key]

    union -- add new key values that does not exists in the source

    select tu.[key],tu.[value],tu.[type] -- take value from #t_update
    from #t_source as ts
    right join #t_update as tu
    on ts.[key] = tu.[key]
    where ts.[key] is null
) as x
where [value] != '' -- remove key-value pair if the value is empty

/*
openjson type column data type
https://docs.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql?view=sql-server-2017

type    data-type
0       null
1       string
2       int
3       true/false
4       array
5       object
*/


-- transform table back to json in a generic way
select @j_source = 
        '{' + 
        STUFF((
                select replace(',"x":','x', cast([key] as varchar(4000)) COLLATE SQL_Latin1_General_CP1_CI_AS) 
                    + case [type]
                        when 1 then replace('"z"','z',[value]) -- this is a string this is a text use double-quotes
                        when 2 then [value]  -- this is int, don't use double-quotes
                        else '' 
                     end
                from #t_result 
                for xml PATH('')
        ), 1, 1, '') 
        + '}'       

print 'after update'
print @j_source

drop table #t_source
drop table #t_update
drop table #t_result
我的解决方案可行,但:

  • 可能无法使用数组或嵌套json。好吧,这一点我不介意

  • 我想知道是否有一种更合适的/情感的/优雅的方式来完成整个解决方案,也许是使用json_modify

  • 键-值对的顺序并没有作为源代码保留,但我想这没什么大不了的

  • 有没有正常的方法可以将键值表转换回json,而无需显式定义列,也无需“for json auto”给出的“垃圾”

  • 代码:

    输出:

    {"key":"address","value":"567 Yonge Ave…"},
    {"key":"age","value":35}, {"key":"first_name","value":"Johnny"},
    {"key":"height","value":1.83},{"key":"last_name","value":"Smith"}
    
    更新:

    基于,我在该解决方案中添加了另一个案例,以在值为[type]=2(int)时排除引号。在我的例子中,当有数百万条记录时,额外的引用会影响存储

    create function dbo.fn_json_merge
    (
        @a nvarchar(max),
        @b nvarchar(max)
    )
    returns nvarchar(max)
    as
    begin
         if left(@a, 1) = '{' and left(@b, 1) = '{' 
         begin
                select
                    @a = case 
                            when d.[type] in (1,3) then json_modify(@a, concat('$.',d.[key]), d.[value]) 
                            else @a 
                        end,
                    @a = case 
                            when d.[type] in (2) and TRY_CAST(d.[value] AS int) is not null then json_modify(@a, concat('$.',d.[key]), cast(d.[value] as int)) 
                            when d.[type] in (2) and TRY_CAST(d.[value] AS int) is null then json_modify(@a, concat('$.',d.[key]), d.[value])
                            else @a 
                        end,
                    @a = case 
                            when d.[type] in (4,5) then json_modify(@a, concat('$.',d.[key]), json_query(d.[value])) 
                            else @a 
                         end
                from openjson(@b) as d;
         end 
         else if left(@a, 1) = '[' and left(@b, 1) = '{' 
         begin
                select @a = json_modify(@a, 'append $', json_query(@b));
         end 
         else 
         begin
                select @a = concat('[', @a, ',', right(@b, len(@b) - 1));
         end;
    
        return @a;
    end;
    
    看一看。 如果您使用Sql Server 2017,则可以创建函数来合并json:

    create function dbo.fn_json_merge
    (
        @a nvarchar(max),
        @b nvarchar(max)
    )
    returns nvarchar(max)
    as
    begin
        if left(@a, 1) = '{' and left(@b, 1) = '{' begin
            select
                @a = case when d.[type] in (4,5) then json_modify(@a, concat('$.',d.[key]), json_query(d.[value])) else @a end,
                @a = case when d.[type] not in (4,5) then json_modify(@a, concat('$.',d.[key]), d.[value]) else @a end
            from openjson(@b) as d;
        end else if left(@a, 1) = '[' and left(@b, 1) = '{' begin
            select @a = json_modify(@a, 'append $', json_query(@b));
        end else begin
            select @a = concat('[', @a, ',', right(@b, len(@b) - 1));
        end;
    
        return @a;
    end;
    

    更新根据评论更新-应能更好地处理不同类型的值

    create function dbo.fn_json_merge
    (
        @a nvarchar(max),
        @b nvarchar(max)
    )
    returns nvarchar(max)
    as
    begin
        if left(@a, 1) = '{' and left(@b, 1) = '{' begin
            select @a =
                case
                    when d.[type] in (4,5) then
                        json_modify(@a, concat('$.',d.[key]), json_query(d.[value]))
                    when d.[type] in (3) then
                        json_modify(@a, concat('$.',d.[key]), cast(d.[value] as bit))
                    when d.[type] in (2) and try_cast(d.[value] as int) = 1 then
                        json_modify(@a, concat('$.',d.[key]), cast(d.[value] as int))
                    when d.[type] in (0) then
                        json_modify(json_modify(@a, concat('lax $.',d.[key]), 'null'), concat('strict $.',d.[key]), null)
                    else
                        json_modify(@a, concat('$.',d.[key]), d.[value])
                end
            from openjson(@b) as d
        end else if left(@a, 1) = '[' and left(@b, 1) = '{' begin
            select @a = json_modify(@a, 'append $', json_query(@b))
        end else begin
            select @a = concat('[', @a, ',', right(@b, len(@b) - 1))
        end
    
        return @a
    end
    

    这是一个非常漂亮、优雅的解决方案!我做了一个小改动,在值为int时排除引号。另外,如果@a为null或为空,它也不能正确处理。@Shawneli很可能是这样,我还没有广泛测试过它float值没有正确处理(转换为字符串)“testFloat”:3.5-->“testFloat”:“3.5”@ErvinS您可以用处理integer的相同方式调整它