65.9K
CodeProject 正在变化。 阅读更多。
Home

将 JSON 文本插入 SQL Server 表

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (28投票s)

2016 年 3 月 25 日

CPOL

7分钟阅读

viewsIcon

308245

downloadIcon

1398

如何轻松地将 JSON 对象数组插入 SQL Server 2016 表中

目录

  1. 引言
  2. 背景
  3. 用例 - 加载 JSON 到数据库
    1. 处理必填字段
    2. 导入复杂的 JSON 结构
  4. 生成代码
  5. 深入了解
  6. 历史

引言

SQL Server 2016 和 Azure SQL Database 有一个新函数 OPENJSON,它使您能够轻松解析 JSON 文本并将其插入到任何表中。有关 JSON 支持的更多详细信息,请参阅 2016 年 7 月 13 日 - JSON 即将登陆 SQL Server。在本文中,我们将了解如何使用此函数将 JSON 对象数组插入到表中。

背景

您是否曾经创建过接受 JSON 的 REST API,并且需要将此 JSON 导入数据库?也许您有一个 REST 服务,该服务接收来自 JQuery、AngularJS 或 ReactJS 应用程序的 JSON?您是否曾经调用过返回 JSON 响应的 REST 服务,或者从文件中加载过 JSON,然后需要将结果存储在 SQL 表中?也许您需要将来自 Twitter 或 MongoDB 的某些 JSON 文档加载到数据库中?

过去,您可能需要使用 JSON.Net 或其他序列化器来解析此 JSON,或者使用框架将 JSON 映射到对象,然后使用 ADO.NET 或 Entity Framework 将它们存储到数据库中。使用 SQL Server 2016,您有另一种选择 - 只需将整个 JSON 文本发送到数据库,并使用新的 OPENJSON 函数进行解析。

在本文中,我们将介绍如何实现这一点。

用例 - 将 JSON 导入数据库

假设您有一个或多个 JSON 对象,如下例所示

[
 { "id" : 2,"firstName": "John", "lastName": "Smith",
   "age": 25, "dateOfBirth": "2007-03-25T12:00:00" },
 { "id" : 5,"firstName": "John", "lastName": "Smith",
   "age": 35, "dateOfBirth": "2005-11-04T12:00:00" },
 { "id" : 7,"firstName": "John", "lastName": "Smith",
   "age": 15, "dateOfBirth": "1983-10-28T12:00:00" },
 { "id" : 8,"firstName": "John", "lastName": "Smith",
   "age": 12, "dateOfBirth": "1995-07-05T12:00:00" },
 { "id" : 9,"firstName": "John", "lastName": "Smith",
   "age": 37, "dateOfBirth": "2015-03-25T12:00:00" }
]

如果您将此 JSON 作为查询或存储过程的参数发送,或者将其设置为本地变量,您就可以使用 OPENJSON 函数轻松地将此 JSON 对象数组转换为一组行,并查看此 JSON 中的内容

 SELECT * 
 FROM OPENJSON(@json)
      WITH (id int, firstName nvarchar(50), lastName nvarchar(50), 
            age int, dateOfBirth datetime2)

OPENJSON 函数将解析 JSON,并在 WITH 子句中,您可以指定您想要看到的列名。OPENJSON 会将列名与 JSON 数组中的键进行匹配,并返回一组行。此外,它还会自动将字符数据转换为与每个列关联的类型。如果您执行此查询,您将获得类似以下的输出

2    John    Smith    25    2007-03-25 12:00:00.0000000
5    John    Smith    35    2005-11-04 12:00:00.0000000
7    John    Smith    15    1983-10-28 12:00:00.0000000
8    John    Smith    12    1995-07-05 12:00:00.0000000
9    John    Smith    37    2015-03-25 12:00:00.0000000

现在,您可以轻松地将这些值导入到任何表中

INSERT INTO Person (id, name, surname, age, dateOfBirth)
 SELECT id, firstNAme, lastName, age, dateOfBirth 
 FROM OPENJSON(@json)
 WITH (id int,
       firstName nvarchar(50), lastName nvarchar(50), 
       age int, dateOfBirth datetime2)

因此,这是一个直接将您的 JSON 导入表的命令。您可以将其放在存储过程中,然后仅提供 JSON 作为输入参数

DROP PROCEDURE IF EXISTS dbo.PersonInsertJson
GO
CREATE PROCEDURE dbo.PersonInsertJson(@Person NVARCHAR(MAX))
AS BEGIN
  INSERT INTO Person (id, name, surname, age, dateOfBirth)
  SELECT id, firstNAme, lastName, age, dateOfBirth
  FROM OPENJSON(@json)
       WITH (id int, firstName nvarchar(50), lastName nvarchar(50), _
             age int, dateOfBirth datetime2)
END

您可以使用类似的存储过程来更新表中的现有行。在以下示例中,我将使用 OPENJSON 来解析输入的 JSON 文本,并通过匹配 id 字段来更新 namesurnameagedateOfBirth 字段

UPDATE Person
 SET name = json.firstname,
 surname = json.lastname,
 age = json.age,
 dateOfBirth = json.dateOfBirth
 FROM OPENJSON(@json)
 WITH (id int,
       firstName nvarchar(50), lastName nvarchar(50), 
       age int, dateOfBirth datetime2) AS json
 WHERE Person.id = json.id

有关此帖子的详细信息,请参阅 OPENJSON – 将 JSON 文本导入表的最简单方法

如果您是高级 SQL 用户,可以使用 MERGE 语句,该语句将在行不存在时插入行,在匹配时更新行。

    MERGE INTO Person AS P 
    USING ( 
        SELECT *
        FROM  OPENJSON(@json)
              WITH (id int, firstName nvarchar(50), lastName nvarchar(50),
                    age int, dateOfBirth datetime2) InputJSON
       ON (P.id = InputJSON.id) 
    WHEN MATCHED THEN 
        UPDATE SET P.firstName = InputJSON.firstName, 
                   P.lastName = InputJSON.lastName, 
                   P.age = InputJSON.age, 
                   P.dateOfBirth = InputJSON.dateOfBirth
    WHEN NOT MATCHED THEN 
        INSERT (firstName, lastName, age, dateOfBirth)  
        VALUES (InputJSON.firstName, InputJSON.lastName, InputJSON.age, InputJSON.dateOfBirth); 

这是一个组合的 UPdate 或 inSERT 命令,它将根据 id 更新行,或添加新行。您可以在 SQL Server 2016 中的 Upsert JSON 文档 中找到更多详细信息。

处理必填字段

JSON 对象中可能缺少某些字段,这可能没问题。如果输入中没有该属性,OPENJSON 将返回 NULL。但是,如果您想确保输入 JSON 中包含所有必填字段,可以在列中添加 strict 选项

SELECT *
FROM OPENJSON(@json)
 WITH (id int 'strict $.id',
       firstName nvarchar(50) 'strict $.firstName',
       lastName nvarchar(50),
       age int,
       dateOfBirth datetime2)

如果您在类型后面添加“$.strict keyName”选项,OPENJSON 将知道 keyName 是必填的。如果它在 keyName 中找不到值,它将抛出错误。

未标记为 strict 的字段不是必填的,如果 OPENJSON 找不到它们,它将返回 null

导入复杂的 JSON 对象

JSON 不必是扁平的。您的 JSON 对象可能包含嵌套值,如下例所示

[
 { "id" : 2,
   "info": { "name": "John", "surame": "Smith" },
   "age": 25 },
  {
   "id" : 5,
   "info": { "name": "Jane", "surame": "Smith" },
   "dateOfBirth": "2005-11-04T12:00:00" }
]

OPENJSON 也可以解析此结构。如果您没有扁平的键值对层次结构,可以在类型后面指定每个属性的“路径”

SELECT *
FROM OPENJSON(@json)
 WITH (id int 'strict $.id',
       firstName nvarchar(50) 'strict $.info.name',
       lastName nvarchar(50) '$.info.surname',
       age int,
       dateOfBirth datetime2)

如果您没有扁平的键值对层次结构,并且某些字段嵌套在对象中,您可以指定类似 JavaScript 的字段路径。您可以将嵌套路径与 strict 关键字结合使用。

JSON 甚至可以包含嵌套数组,如下例所示

[
  { "id" : 2,
   "info": { "name": "John", "surame": "Smith" },
   "age": 25,
   "skills": ["C#","SQL","JSON","REST"]
  },
  {
   "id" : 5,
   "info": { "name": "Jane", "surame": "Smith" },
   "dateOfBirth": "2005-11-04T12:00:00",
  "skills": ["C#","SQL"] }
]

您也可以使用 OPENJSON 解析此 JSON 文本。由于我们想将此 skills JSON 数组作为“整个 JSON”读取,因此需要添加 AS JSON 选项

SELECT *
FROM OPENJSON(@json)
 WITH (id int 'strict $.id',
       firstName nvarchar(50) 'strict $.info.name',
       lastName nvarchar(50) '$.info.surname',
       age int,
       dateOfBirth datetime2,
       skills NVARCHAR(MAX) AS JSON
)

现在 OPENJSON 将返回完整的 JSON 数组作为 skills 列

id firstName lastName age  dateOfBirth                 skills
2  John      Smith    25   NULL                        ["C#","SQL","JSON","REST"]
5  Jane      Smith    NULL 2005-11-04 12:00:00.0000000 ["C#","SQL"]

正如您所看到的,通过一些选项,您可以轻松读取任何 JSON 结构并验证必填字段。

生成插入 JSON 的代码

尽管这是一个简单的命令,但如果您的表有 20-30 列,则可能难以编写。此外,如果某些列包含特殊字符,您需要在 SQL 名称中使用 [ ],在 JSON 路径中使用“ ”。

因此,我创建了一个函数来生成此脚本 - 您可以在此处下载。此 SQL 函数的签名如下所示

CREATE FUNCTION
dbo.GenerateJsonInsertProcedure(@SchemaName sysname, @TableName sysname, _
    @JsonColumns nvarchar(max), @IgnoredColumns nvarchar(max))
RETURNS NVARCHAR(MAX)

为了生成 Insert 存储过程,您可以指定表的架构名称和表名。此外,如果您有包含 JSON 文本的表列,并且您的输入中将包含嵌套 JSON,则可以在 @JsonColumns 参数中指定这些列的列表。最后,如果某些列不应通过 JSON 插入(例如,DateModifiedModifiedBy),您可以将它们提供为逗号分隔的列名列表。

现在,让我们看看它是如何工作的。我将为 AdventureWorks Person.Address 表生成 JSON insert 存储过程

declare @SchemaName sysname = 'Person' --> Name of the table where we want to insert JSON
declare @TableName sysname = _
    'Address' --> Name of the table schema where we want to insert JSON
declare @IgnoredColumns nvarchar(max) = _
    'DateModified' --> List of columns that should be ignored
-- comma separated column names in this list will not be included in WITH schema 
declare @JsonColumns nvarchar(max) = '||' --> List of pipe-separated NVARCHAR(MAX) 
--column names that contain JSON text, e.g. '|AdditionalInfo|Demographics|'

print (dbo.GenerateJsonInsertProcedure(@SchemaName, @TableName, @JsonColumns, @IgnoredColumns))

在这种情况下,我只会打印函数返回的脚本。输出将是

DROP PROCEDURE IF EXISTS [Person].[AddressInsertJson]
GO
CREATE PROCEDURE [Person].[AddressInsertJson](@Address NVARCHAR(MAX))
AS BEGIN
INSERT INTO Address([AddressLine1],[AddressLine2],[City],[StateProvinceID],_
                    [PostalCode],[ModifiedDate])
 SELECT [AddressLine1],[AddressLine2],[City],[StateProvinceID],[PostalCode],[ModifiedDate]
 FROM OPENJSON(@AddressJson)
 WITH (
 [AddressLine1] nvarchar(120) N'strict $."AddressLine1"',
 [AddressLine2] nvarchar(120) N'$."AddressLine2"',
 [City] nvarchar(60) N'strict $."City"',
 [StateProvinceID] int N'strict $."StateProvinceID"',
 [PostalCode] nvarchar(30) N'strict $."PostalCode"',
 [ModifiedDate] datetime N'strict $."ModifiedDate"')
END

该函数将遍历指定表中的所有列,检查其类型,是否为必填列(在这种情况下,它将在路径中生成 $.strict 修饰符)并创建脚本。您可以修改此查询并删除不必要的列。

如果您想尝试一下,可以 下载 GenerateJsonInsertScript.zip,其中包含 SQL 脚本。

脚本内部

如果您想了解此脚本的详细信息,这里有一些更详细的解释。

首先,我们需要一个查询来返回将在 INSERT 列表、SELECT 列表和 WITH 子句中生成的列列表。我使用了这个查询

select
    col.name as ColumnName,
    column_id ColumnId,
    typ.name as ColumnType,
 -- create type with size based on type name and size
 case typ.name
  when 'char' then '(' + cast(col.max_length as varchar(10))+ ')'
        when 'nchar' then '(' + cast(col.max_length as varchar(10))+ ')'
        when 'nvarchar' then (IIF(col.max_length=-1, '(MAX)', _
        '(' + cast(col.max_length as varchar(10))+ ')'))
        when 'varbinary' then (IIF(col.max_length=-1, '(MAX)', _
        '(' + cast(col.max_length as varchar(10))+ ')'))
        when 'varchar' then (IIF(col.max_length=-1, '(MAX)', _
        '(' + cast(col.max_length as varchar(10))+ ')'))
  else ''
 end as StringSize,
 -- if column is not nullable, add Strict mode in JSON
    case
        when col.is_nullable = 1 then '$.' else 'strict $.'
    end Mode,
 CHARINDEX(col.name, @JsonColumns,0) as IsJson
from sys.columns col
    join sys.types typ on
        col.system_type_id = typ.system_type_id AND col.user_type_id = typ.user_type_id
   LEFT JOIN dbo.syscomments SM ON col.default_object_id = SM.id 
where object_id = object_id(QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName))
-- Do not insert identity, computed columns, hidden columns, 
-- rowguid columns, generated always columns
-- Skip columns that cannot be parsed by JSON, e.g. text, sql_variant, etc.
and col.is_identity = 0
and col.is_computed = 0
and col.is_hidden = 0
and col.is_rowguidcol = 0
and generated_always_type = 0
and (sm.text IS NULL OR sm.text NOT LIKE '(NEXT VALUE FOR%')
and LOWER(typ.name) _
    NOT IN ('text', 'ntext', 'sql_variant', 'image','hierarchyid','geometry','geography')
and col.name NOT IN (SELECT value FROM STRING_SPLIT(@IgnoredColumns, ','))

此查询将通过查看系统表来返回列及其 id、名称和类型列表。

有趣的是 StringSize 列。此列根据 typelength 格式化 WITH 子句中的类型,例如 nvarchar(50)varchar(max)Mode 列如果列是必填的(即非 null),则返回“strict $”而不是“$”。此模式要求键必须存在于 JSON 中。@jsonColumns 变量中的所有列将在 IsJson 列中标记为“1”。

此代码不会返回不应通过显式插入插入到表中的列。在此代码中,我排除了标识列、hidden 列、computed 列、rowguid 列和始终生成的列。此外,我还排除了所有将通过序列填充的列。

此外,如果 OPENJSON 无法返回某些类型(如 CLR、geometry/geography),此查询将忽略这些列。

最后,所有放置在 IgnoredColumns 中的列都将被忽略。

现在我需要生成属于表架构的列列表,该列表将添加到存储过程的 INSERT (<<column list>>) 和 SELECT <<column list>> 部分。我正在使用此脚本

declare @TableSchema nvarchar(max) = '';

select @TableSchema = @TableSchema + QUOTENAME(ColumnName) + ','
from <<col_def>>
order by ColumnId

SET @TableSchema = SUBSTRING(@TableSchema, 0, LEN(@TableSchema)) --> remove last comma

<<col_def>> 是之前的查询(在我的脚本中用作 CTE)。此代码将连接该查询中的所有列名,并将它们作为逗号分隔的字符串返回。

现在我需要生成 OPENJSON 函数的 WITH 子句中的列、类型和 json 路径。这是查询

declare @JsonSchema nvarchar(max) = '';

select @JsonSchema = @JsonSchema + '
 ' + QUOTENAME(ColumnName) + ' ' + ColumnType + StringSize +
 N''' + Mode + '"' + STRING_ESCAPE(ColumnName, 'json') + _
        '"''' +IIF(IsJson>0, ' AS JSON', '') + ','
from col_def
order by ColumnId

此查询与上一个查询类似。在这里,我正在生成以下序列

column_name type json_path [AS JSON],

最后,我需要将这两个列列表注入到 INSERT SELECT OPENJSON WITH() 脚本中,并生成将由存储过程返回的脚本

declare @Result nvarchar(max) =
N'DROP PROCEDURE IF EXISTS ' + QUOTENAME( @SchemaName) + '.' + _
       QUOTENAME(@TableName + 'InsertJson') + '
GO
CREATE PROCEDURE ' + QUOTENAME( @SchemaName) + '.' + _
       QUOTENAME(@TableName + 'InsertJson') + '(@' + @TableName + ' NVARCHAR(MAX))
AS BEGIN

 INSERT INTO ' + @TableName + '(' + @TableSchema + ')
 SELECT ' + @TableSchema + '
 FROM OPENJSON(' + @JsonParam + ')
  WITH (' + @JsonSchema + ')
END'

RETURN REPLACE(@Result,',)',')')

最后的 replace 命令用于删除 WITH 子句中的最后一个逗号。

如果您只想调用此存储过程,则不需要这些详细信息;但是,如果您计划修改它,您将需要这些详细信息。

历史

  • 2016 年 3 月 25 日 - 初始版本
  • 2016 年 3 月 26 日 - 增加了关于生成代码函数实现细节的说明
© . All rights reserved.