SQL Server:从另一个json更新json - 通常

时间:2018-06-06 20:40:56

标签: json sql-server sql-server-json

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

一些背景知识:我将元数据存储为varchar(max)列中的json。每个记录可以在同一个表中具有不同的元数据键。就像在同一个表中存储人员和产品一样。与EAV data model类似,但我使用json列将元数据存储为键值对,而不是值表。 这就是我正在寻找通用解决方案的原因。

即。一条记录可以有元数据

{"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中的多个值。

即。来源

{
    "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

我的解决方案有效,但是:

  1. 可能无法使用数组或嵌套的json。很好,此时并没有打扰我。

  2. 我想知道是否有更合适/更有效/更优雅的方式来完成整个解决方案,也许使用json_modify?

  3. 键值对的顺序不作为源,但我想这不是什么大问题。

  4. 将键值表转换回json的任何常规方法,无需显式定义列,也不使用" garbage" "对于json auto"给出

  5. 代码:

    SELECT [key], [value] 
    FROM t_result 
    FOR JSON path, WITHOUT_ARRAY_WRAPPER
    

    输出:

    {"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"}
    

    更新:

    根据Roman Pekar elegant solution,我在该解决方案中添加了另一个案例,以便在值为[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;
    

1 个答案:

答案 0 :(得分:1)

看看this answer。 如果您在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;

sql fiddle demo

更新根据评论进行了更新-应该更好地与不同类型的值配合使用

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

sql fiddle demo