refactor(data-access): replace TVP lookups with STRING_SPLIT and OPENJSON

Remove dependency on deleted SQL Server Table-Valued Parameter types
by refactoring lookup methods to use built-in SQL Server functions:

- Simple single-value lookups (Items, WorkOrders, WorkCenters,
  ProfitCenters, Users) now use STRING_SPLIT with comma-separated
  strings from C#
- Complex multi-column lookup (Lots with LotNumber + ItemNumber)
  now uses OPENJSON with JSON string from C#

This eliminates the need for TVP type definitions (scripts 033-039)
while maintaining equivalent functionality.
This commit is contained in:
Joseph Doherty
2026-01-06 15:34:39 -05:00
parent c6aeb20d9c
commit 645ef46f35
2 changed files with 29 additions and 56 deletions
@@ -20,7 +20,7 @@ public static partial class LotFinderQueries
ORDER BY i.ItemNumber"; ORDER BY i.ItemNumber";
/// <summary> /// <summary>
/// Looks up items by exact item numbers using table-valued parameter. /// Looks up items by exact item numbers using STRING_SPLIT.
/// </summary> /// </summary>
public const string SqlLookupItems = @" public const string SqlLookupItems = @"
SELECT i.ShortItemNumber, SELECT i.ShortItemNumber,
@@ -28,16 +28,16 @@ public static partial class LotFinderQueries
i.Description, i.Description,
i.LastUpdateDT i.LastUpdateDT
FROM dbo.Item AS i FROM dbo.Item AS i
INNER JOIN @itemNumbers AS i2 ON (i.ItemNumber = i2.ItemNumber) WHERE i.ItemNumber IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@itemNumbers, ','))
ORDER BY i.ItemNumber"; ORDER BY i.ItemNumber";
/// <summary> /// <summary>
/// Looks up work orders by work order numbers using table-valued parameter. /// Looks up work orders by work order numbers using STRING_SPLIT.
/// </summary> /// </summary>
public const string SqlLookupWorkorders = @" public const string SqlLookupWorkorders = @"
SELECT * SELECT *
FROM dbo.WorkOrder AS wo FROM dbo.WorkOrder AS wo
INNER JOIN @workOrderNumbers wo2 ON (wo.WorkOrderNumber = wo2.WorkOrderNumber)"; WHERE wo.WorkOrderNumber IN (SELECT CAST(LTRIM(RTRIM(value)) AS BIGINT) FROM STRING_SPLIT(@workOrderNumbers, ','))";
/// <summary> /// <summary>
/// Searches work centers by code or description. /// Searches work centers by code or description.
@@ -53,14 +53,14 @@ public static partial class LotFinderQueries
ORDER BY wc.Code"; ORDER BY wc.Code";
/// <summary> /// <summary>
/// Looks up work centers by codes using table-valued parameter. /// Looks up work centers by codes using STRING_SPLIT.
/// </summary> /// </summary>
public const string SqlLookupWorkCenters = @" public const string SqlLookupWorkCenters = @"
SELECT wc.Code, SELECT wc.Code,
wc.Description, wc.Description,
wc.LastUpdateDT wc.LastUpdateDT
FROM dbo.WorkCenter AS wc FROM dbo.WorkCenter AS wc
INNER JOIN @workCenterCodes wc2 ON (wc.Code = wc2.Code) WHERE wc.Code IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@workCenterCodes, ','))
ORDER BY wc.Code"; ORDER BY wc.Code";
/// <summary> /// <summary>
@@ -77,14 +77,14 @@ public static partial class LotFinderQueries
ORDER BY pc.Code"; ORDER BY pc.Code";
/// <summary> /// <summary>
/// Looks up profit centers by codes using table-valued parameter. /// Looks up profit centers by codes using STRING_SPLIT.
/// </summary> /// </summary>
public const string SqlLookupProfitCenters = @" public const string SqlLookupProfitCenters = @"
SELECT pc.Code, SELECT pc.Code,
pc.Description, pc.Description,
pc.LastUpdateDT pc.LastUpdateDT
FROM dbo.ProfitCenter AS pc FROM dbo.ProfitCenter AS pc
INNER JOIN @profitCenterCodes AS pc2 ON (pc.Code = pc2.Code) WHERE pc.Code IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@profitCenterCodes, ','))
ORDER BY pc.Code"; ORDER BY pc.Code";
/// <summary> /// <summary>
@@ -103,7 +103,7 @@ public static partial class LotFinderQueries
ORDER BY u.UserID, u.FullName"; ORDER BY u.UserID, u.FullName";
/// <summary> /// <summary>
/// Looks up users by user IDs or address numbers using table-valued parameter. /// Looks up users by user IDs or address numbers using STRING_SPLIT.
/// </summary> /// </summary>
public const string SqlLookupUsers = @" public const string SqlLookupUsers = @"
SELECT u.AddressNumber, SELECT u.AddressNumber,
@@ -111,11 +111,12 @@ public static partial class LotFinderQueries
u.FullName, u.FullName,
u.LastUpdateDT u.LastUpdateDT
FROM dbo.JdeUser AS u FROM dbo.JdeUser AS u
INNER JOIN @userIDs u2 ON (u.UserID = u2.UserName OR CAST(u.AddressNumber AS VARCHAR(20)) = u2.UserName) WHERE u.UserID IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@userIds, ','))
OR CAST(u.AddressNumber AS VARCHAR(20)) IN (SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@userIds, ','))
ORDER BY u.UserID"; ORDER BY u.UserID";
/// <summary> /// <summary>
/// Looks up lots by lot number and item number using table-valued parameter. /// Looks up lots by lot number and item number using OPENJSON.
/// </summary> /// </summary>
public const string SqlLookupLots = @" public const string SqlLookupLots = @"
SELECT DISTINCT SELECT DISTINCT
@@ -126,6 +127,9 @@ public static partial class LotFinderQueries
l.SupplierCode, l.SupplierCode,
l.LastUpdateDT l.LastUpdateDT
FROM dbo.Lot AS l FROM dbo.Lot AS l
INNER JOIN @lotNumbers ln ON (l.LotNumber = ln.ComponentLotNumber AND INNER JOIN OPENJSON(@lotsJson) WITH (
LotNumber VARCHAR(30) '$.LotNumber',
ItemNumber VARCHAR(128) '$.ItemNumber'
) ln ON (l.LotNumber = ln.LotNumber AND
((l.ItemNumber IS NULL AND ln.ItemNumber IS NULL) OR l.ItemNumber = ln.ItemNumber))"; ((l.ItemNumber IS NULL AND ln.ItemNumber IS NULL) OR l.ItemNumber = ln.ItemNumber))";
} }
@@ -1,4 +1,3 @@
using System.Data;
using Dapper; using Dapper;
using JdeScoping.Core.Models; using JdeScoping.Core.Models;
using JdeScoping.Core.Models.Inventory; using JdeScoping.Core.Models.Inventory;
@@ -44,17 +43,12 @@ public partial class LotFinderRepository
const string operation = nameof(LookupItemsAsync); const string operation = nameof(LookupItemsAsync);
try try
{ {
var dataTable = new DataTable(); var itemNumbersCsv = string.Join(",", itemNumbers);
dataTable.Columns.Add("ItemNumber", typeof(string));
foreach (var itemNumber in itemNumbers)
{
dataTable.Rows.Add(itemNumber);
}
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<Item>( var result = await connection.QueryAsync<Item>(
LotFinderQueries.SqlLookupItems, LotFinderQueries.SqlLookupItems,
new { itemNumbers = dataTable.AsTableValuedParameter("ItemNumberFilterParameter") }, new { itemNumbers = itemNumbersCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds); commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList(); return result.ToList();
} }
@@ -75,17 +69,12 @@ public partial class LotFinderRepository
const string operation = nameof(LookupWorkordersAsync); const string operation = nameof(LookupWorkordersAsync);
try try
{ {
var dataTable = new DataTable(); var workOrderNumbersCsv = string.Join(",", workorderNumbers);
dataTable.Columns.Add("WorkOrderNumber", typeof(long));
foreach (var workOrderNumber in workorderNumbers)
{
dataTable.Rows.Add(workOrderNumber);
}
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<WorkOrder>( var result = await connection.QueryAsync<WorkOrder>(
LotFinderQueries.SqlLookupWorkorders, LotFinderQueries.SqlLookupWorkorders,
new { workOrderNumbers = dataTable.AsTableValuedParameter("WorkOrderFilterParameter") }, new { workOrderNumbers = workOrderNumbersCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds); commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList(); return result.ToList();
} }
@@ -130,17 +119,12 @@ public partial class LotFinderRepository
const string operation = nameof(LookupWorkCentersAsync); const string operation = nameof(LookupWorkCentersAsync);
try try
{ {
var dataTable = new DataTable(); var workCenterCodesCsv = string.Join(",", codes);
dataTable.Columns.Add("Code", typeof(string));
foreach (var code in codes)
{
dataTable.Rows.Add(code);
}
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<WorkCenter>( var result = await connection.QueryAsync<WorkCenter>(
LotFinderQueries.SqlLookupWorkCenters, LotFinderQueries.SqlLookupWorkCenters,
new { workCenterCodes = dataTable.AsTableValuedParameter("WorkCenterFilterParameter") }, new { workCenterCodes = workCenterCodesCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds); commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList(); return result.ToList();
} }
@@ -185,17 +169,12 @@ public partial class LotFinderRepository
const string operation = nameof(LookupProfitCentersAsync); const string operation = nameof(LookupProfitCentersAsync);
try try
{ {
var dataTable = new DataTable(); var profitCenterCodesCsv = string.Join(",", codes);
dataTable.Columns.Add("Code", typeof(string));
foreach (var code in codes)
{
dataTable.Rows.Add(code);
}
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<ProfitCenter>( var result = await connection.QueryAsync<ProfitCenter>(
LotFinderQueries.SqlLookupProfitCenters, LotFinderQueries.SqlLookupProfitCenters,
new { profitCenterCodes = dataTable.AsTableValuedParameter("ProfitCenterFilterParameter") }, new { profitCenterCodes = profitCenterCodesCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds); commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList(); return result.ToList();
} }
@@ -240,17 +219,12 @@ public partial class LotFinderRepository
const string operation = nameof(LookupUsersAsync); const string operation = nameof(LookupUsersAsync);
try try
{ {
var dataTable = new DataTable(); var userIdsCsv = string.Join(",", userIds);
dataTable.Columns.Add("UserName", typeof(string));
foreach (var userId in userIds)
{
dataTable.Rows.Add(userId);
}
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<JdeUser>( var result = await connection.QueryAsync<JdeUser>(
LotFinderQueries.SqlLookupUsers, LotFinderQueries.SqlLookupUsers,
new { userIDs = dataTable.AsTableValuedParameter("OperatorFilterParameter") }, new { userIds = userIdsCsv },
commandTimeout: _options.Value.DefaultTimeoutSeconds); commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList(); return result.ToList();
} }
@@ -271,18 +245,13 @@ public partial class LotFinderRepository
const string operation = nameof(LookupLotsAsync); const string operation = nameof(LookupLotsAsync);
try try
{ {
var dataTable = new DataTable(); var lotsJson = System.Text.Json.JsonSerializer.Serialize(
dataTable.Columns.Add("ComponentLotNumber", typeof(string)); lots.Select(l => new { l.LotNumber, l.ItemNumber }));
dataTable.Columns.Add("ItemNumber", typeof(string));
foreach (var lot in lots)
{
dataTable.Rows.Add(lot.LotNumber, lot.ItemNumber);
}
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct); await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
var result = await connection.QueryAsync<Lot>( var result = await connection.QueryAsync<Lot>(
LotFinderQueries.SqlLookupLots, LotFinderQueries.SqlLookupLots,
new { lotNumbers = dataTable.AsTableValuedParameter("ComponentLotFilterParameter") }, new { lotsJson },
commandTimeout: _options.Value.DefaultTimeoutSeconds); commandTimeout: _options.Value.DefaultTimeoutSeconds);
return result.ToList(); return result.ToList();
} }