Using JSON to Move Beyond TVPs for Multi-Row Updates
Many .NET applications will use Table-Valued-Parameters (TVPs) to send in a set of rows to be updated to a single stored procedure. Using this approach requires a decent amount of legwork to achieve:
- Define a
TYPE
in SQL Server to represent the table valued parameter - Assign permissions to the TYPE so that it can be used within the proc
- Write the proc to include the TVP and use it appropriately
- Format an ADO.NET
DataTable
class in C# code (and make sure to get the type conversions right)
This is a LOT of overhead (IMHO) to achieve a set of rows being affected in a single sproc call.
Fortunately, there are alternatives that can make this easier to achieve with fewer artifacts (namely no TYPE
and no special permissions are required): XML and JSON. This post will focus on the JSON technique, which is available with SQL Server 2016 and later.
Consider a table with the following columns:
AccountFieldId
: unique key for the tableAccountId
: foreign key to the account tableFieldId
: foreign key to a field tableValue
: value for a given field and a given account
We would like to update a batch of rows for a single account, and multiple new values for fields.
Desired parameters for the proc:
AccountId
- JSON for the FieldIds and Values
The JSON should probably look something like this:
1[
2 { "FieldId": 123,
3 "FieldValue": "set-to-this-value"
4 },
5 { "FieldId": 456,
6 "FieldValue": "here is another value"
7 }
8]
Sproc Syntax
The basic strategy with sprocs doing a batch of updates is getting the JSON into a temp table (or table variable) so that your update statement just joins to the temp table you’ve created from the JSON. Here is the syntax that can accept the parameters mentioned above and load the JSON into a table variable, and then merge the results into the actual AccountField
table:
1CREATE PROCEDURE [dbo].[AddUpdateAccountFieldFromJson] (
2 @AccountId UNIQUEIDENTIFIER
3 ,@JsonInput NVARCHAR(MAX)
4)
5 AS
6 BEGIN
7 SET NOCOUNT ON;
8
9 DECLARE @Source TABLE (
10 [AccountID] [uniqueidentifier] NOT NULL,
11 [FieldID] [bigint] NOT NULL,
12 [Value] [nvarchar](max) NULL
13 )
14
15 INSERT INTO @Source ([AccountID],[FieldID],[Value])
16 SELECT @AccountId,[FieldId],[Value]
17 FROM OPENJSON(@JsonInput)
18 WITH (
19 [FieldId] BIGINT
20 ,[FieldValue] NVARCHAR(MAX)
21 )
22
23MERGE AccountField AS target
24 USING (SELECT [AccountID],[FieldID],[Value]
25 FROM @Source) AS source
26 ON ( target.[AccountId] = source.[AccountId]
27 AND target.[FieldId] = source.[FieldId] )
28 WHEN MATCHED AND (source.[Value] <> target.[Value])THEN
29 UPDATE SET Value = source.Value
30 WHEN NOT MATCHED THEN
31 INSERT ( [AccountId], [FieldId], [Value] )
32 VALUES ( @AccountId, source.FieldId, source.Value );
33END
C# Code Syntax
The final step in getting batch updates to work is having some code that can CALL the sproc above.
You may be able to do this with an anonymous class, but here is a class definition that can surface the JSON we’re after:
1public class FieldData
2{
3 public long FieldId { get; set; }
4 public string FieldValue { get; set; }
5}
Given that class, here is some code that uses Newtonsoft.JSON and Dapper to call the proc above with the inputs that we want:
1var accountId = "1234-4567-8901";
2var listOfFieldValues = new List<FieldData> {
3 new FieldData { FieldId = 123, FieldValue = "set-to-this-value" },
4 new FieldData { FieldId = 456, FieldValue = "here is another value" }
5};
6
7var jsonInput = JsonConvert.SerializeObject(listOfFieldValues);
8db.Execute("AddUpdateAccountFieldFromJson", new { accountId, jsonInput }, commandType: CommandType.StoredProcedure);
Voila! :)