From a5cd17befb36db72384745ccc31b7afdf9392077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Ferle=C5=BE?= Date: Thu, 16 May 2024 11:13:53 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9Efix:=20Output=20mapping=20for=20tab?= =?UTF-8?q?les=20with=20triggers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Using output mapping following Insert or Update queries on tables with triggers failed due to SQL server constraints. --- .../Simpleverse.Repository.Db.csproj | 6 +- .../SqlServer/BulkExtensions.cs | 169 +++++++++++------- 2 files changed, 103 insertions(+), 72 deletions(-) diff --git a/src/Simpleverse.Repository.Db/Simpleverse.Repository.Db.csproj b/src/Simpleverse.Repository.Db/Simpleverse.Repository.Db.csproj index 868f464..6779557 100644 --- a/src/Simpleverse.Repository.Db/Simpleverse.Repository.Db.csproj +++ b/src/Simpleverse.Repository.Db/Simpleverse.Repository.Db.csproj @@ -13,10 +13,10 @@ true Dapper, Bulk, Merge, Upsert, Delete, Insert, Update, Repository LICENSE - 2.1.11 + 2.1.12 High performance operation for MS SQL Server built for Dapper ORM. Including bulk operations Insert, Update, Delete, Get as well as Upsert both single and bulk. - 2.1.11.0 - 2.1.11.0 + 2.1.12.0 + 2.1.12.0 https://github.com/lukaferlez/Simpleverse.Repository README.md true diff --git a/src/Simpleverse.Repository.Db/SqlServer/BulkExtensions.cs b/src/Simpleverse.Repository.Db/SqlServer/BulkExtensions.cs index 759ca17..2666f0d 100644 --- a/src/Simpleverse.Repository.Db/SqlServer/BulkExtensions.cs +++ b/src/Simpleverse.Repository.Db/SqlServer/BulkExtensions.cs @@ -55,19 +55,10 @@ public static async Task TransferBulkAsync( if (!columnsToCopy.Any()) return string.Empty; - var insertedTableName = $"#tbl_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"; - if (connection.State != ConnectionState.Open) throw new ArgumentException("Connection is required to be opened by the calling code."); - connection.Execute( - $@"SELECT TOP 0 {columnsToCopy.ColumnList()} INTO {insertedTableName} FROM {tableName} WITH(NOLOCK) - UNION ALL - SELECT TOP 0 {columnsToCopy.ColumnList()} FROM {tableName} WITH(NOLOCK); - " - , null - , transaction - ); + var insertedTableName = CreateTemporaryTableFromTable(connection, tableName, columnsToCopy, transaction); if (columnsToCopy.Count() * entitiesToInsert.Count() < 2000 || !(connection is SqlConnection)) { @@ -234,10 +225,12 @@ public async static Task InsertBulkAsync( if (entityCount == 0) return 0; - var mapGeneratedValues = outputMap != null; var typeMeta = TypeMeta.Get(); - var outputEntities = new List(); + var mapGeneratedValues = outputMap != null; + if (mapGeneratedValues && !typeMeta.PropertiesKeyAndExplicit.Any()) + throw new NotSupportedException("Output mapping inserted values is not supported without either a key or explicitkey"); + var result = await connection.ExecuteAsync( entitiesToInsert, @@ -247,43 +240,47 @@ await connection.ExecuteAsync( var columnList = properties.ColumnList(); var query = $@" - INSERT INTO {typeMeta.TableName} ({columnList}) - {(mapGeneratedValues ? OutputClause() : string.Empty)} + INSERT INTO {typeMeta.TableName} ({columnList}) + /**OUTPUT**/ SELECT {columnList} FROM {source} AS Source; "; - if (mapGeneratedValues) + var outputClause = string.Empty; + var outputSource = source; + if (mapGeneratedValues && typeMeta.PropertiesKey.Any()) { - var results = await connection.QueryAsync( - query, - param: parameters, - commandTimeout: commandTimeout, - transaction: transaction + outputSource = CreateTemporaryTableFromTable( + connection, + typeMeta.TableName, + typeMeta.PropertiesKeyAndExplicit, + transaction ); - outputEntities.AddRange(results); - return results.Count(); + + outputClause = OutputClause(outputSource, typeMeta.PropertiesKeyAndExplicit); } - else + + var resultCount = await connection.ExecuteAsync( + query.Replace("/**OUTPUT**/", outputClause), + param: parameters, + commandTimeout: commandTimeout, + transaction: transaction + ); + + if (mapGeneratedValues && resultCount > 0) { - return await connection.ExecuteAsync( - query, - param: parameters, - commandTimeout: commandTimeout, - transaction: transaction + var result = await connection.SelectEntitiesFromSource(outputSource, parameters, transaction, commandTimeout); + outputMap( + entitiesToInsert, + result, + typeMeta.PropertiesExceptKeyAndComputed, + typeMeta.Properties ); } + + return resultCount; }, transaction: transaction, sqlBulkCopy: sqlBulkCopy - ) - ; - - if (mapGeneratedValues) - outputMap( - entitiesToInsert, - outputEntities, - typeMeta.PropertiesExceptKeyAndComputed, - typeMeta.PropertiesKeyAndComputed ); return result; @@ -329,46 +326,34 @@ await connection.ExecuteAsync( UPDATE Target SET {typeMeta.PropertiesExceptKeyAndComputed.ColumnListEquals(", ")} - {(mapGeneratedValues ? OutputClause() : string.Empty)} FROM {source} AS Source INNER JOIN {typeMeta.TableName} AS Target ON {typeMeta.PropertiesKeyAndExplicit.ColumnListEquals(" AND ")}; "; - if (mapGeneratedValues) - { - var results = await connection.QueryAsync( - query, - param: parameters, - commandTimeout: commandTimeout, - transaction: transaction - ); + var resultCount = await connection.ExecuteAsync( + query, + param: parameters, + commandTimeout: commandTimeout, + transaction: transaction + ); - outputValues.AddRange(results); - return results.Count(); - } - else + if (mapGeneratedValues && resultCount > 0) { - return await connection.ExecuteAsync( - query, - param: parameters, - commandTimeout: commandTimeout, - transaction: transaction + var result = await connection.SelectEntitiesFromSource(source, parameters, transaction, commandTimeout); + outputMap( + entitiesToUpdate, + result, + typeMeta.PropertiesKeyAndExplicit, + typeMeta.Properties ); } + + return resultCount; }, transaction: transaction, sqlBulkCopy: sqlBulkCopy - ) - ; - - if (mapGeneratedValues) - outputMap( - entitiesToUpdate, - outputValues, - typeMeta.PropertiesKeyAndExplicit, - typeMeta.PropertiesComputed ); return result; @@ -423,13 +408,59 @@ INNER JOIN {typeMeta.TableName} AS Target return result; } - private static string OutputClause(IEnumerable properties = null) + private static string OutputClause(string targetTable = null, IEnumerable properties = null) + { + var columns = properties?.ColumnList(prefix: "inserted"); + if (string.IsNullOrWhiteSpace(columns)) + columns = "inserted.*"; + + var clause = "OUTPUT " + columns; + if (!string.IsNullOrWhiteSpace(targetTable)) + clause += $" INTO {targetTable}"; + + return clause; + } + + private static async Task> SelectEntitiesFromSource( + this IDbConnection connection, + string source, + DynamicParameters parameters, + IDbTransaction transaction, + int? commandTimeout + ) + where T : class + { + var typeMeta = TypeMeta.Get(); + + var query = $@" + SELECT Target.* + FROM + {source} AS Source + INNER JOIN {typeMeta.TableName} AS Target + ON {typeMeta.PropertiesKeyAndExplicit.ColumnListEquals(" AND ")}; + "; + + return await connection.QueryAsync( + query, + param: parameters, + commandTimeout: commandTimeout, + transaction: transaction + ); + } + + private static string CreateTemporaryTableFromTable(IDbConnection connection, string tableName, IEnumerable columns, IDbTransaction transaction) { - var keyColumns = properties?.ColumnList(prefix: "inserted"); - if (string.IsNullOrWhiteSpace(keyColumns)) - return "OUTPUT inserted.*"; + var insertedTableName = $"#tbl_{Guid.NewGuid().ToString().Replace("-", string.Empty)}"; - return "OUTPUT " + keyColumns; + connection.Execute( + $@"SELECT TOP 0 {columns.ColumnList()} INTO {insertedTableName} FROM {tableName} WITH(NOLOCK) + UNION ALL + SELECT TOP 0 {columns.ColumnList()} FROM {tableName} WITH(NOLOCK); + " + , null + , transaction + ); + return insertedTableName; } public static async Task<(string source, DynamicParameters parameters)> BulkSourceAsync(