Skip to content

Commit

Permalink
🐞fix: Output mapping for tables with triggers
Browse files Browse the repository at this point in the history
Using output mapping following Insert or Update queries on tables with triggers failed due to SQL server constraints.
  • Loading branch information
lukaferlez committed May 16, 2024
1 parent e121265 commit a5cd17b
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageTags>Dapper, Bulk, Merge, Upsert, Delete, Insert, Update, Repository</PackageTags>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Version>2.1.11</Version>
<Version>2.1.12</Version>
<Description>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.</Description>
<AssemblyVersion>2.1.11.0</AssemblyVersion>
<FileVersion>2.1.11.0</FileVersion>
<AssemblyVersion>2.1.12.0</AssemblyVersion>
<FileVersion>2.1.12.0</FileVersion>
<RepositoryUrl>https://github.com/lukaferlez/Simpleverse.Repository</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<EmbedAllSources>true</EmbedAllSources>
Expand Down
169 changes: 100 additions & 69 deletions src/Simpleverse.Repository.Db/SqlServer/BulkExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,10 @@ public static async Task<string> TransferBulkAsync<T>(
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))
{
Expand Down Expand Up @@ -234,10 +225,12 @@ public async static Task<int> InsertBulkAsync<T>(
if (entityCount == 0)
return 0;

var mapGeneratedValues = outputMap != null;
var typeMeta = TypeMeta.Get<T>();

var outputEntities = new List<T>();
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,
Expand All @@ -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<T>(
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<T>(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;
Expand Down Expand Up @@ -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<T>(
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<T>(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;
Expand Down Expand Up @@ -423,13 +408,59 @@ INNER JOIN {typeMeta.TableName} AS Target
return result;
}

private static string OutputClause(IEnumerable<PropertyInfo> properties = null)
private static string OutputClause(string targetTable = null, IEnumerable<PropertyInfo> 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<IEnumerable<T>> SelectEntitiesFromSource<T>(
this IDbConnection connection,
string source,
DynamicParameters parameters,
IDbTransaction transaction,
int? commandTimeout
)
where T : class
{
var typeMeta = TypeMeta.Get<T>();

var query = $@"
SELECT Target.*
FROM
{source} AS Source
INNER JOIN {typeMeta.TableName} AS Target
ON {typeMeta.PropertiesKeyAndExplicit.ColumnListEquals(" AND ")};
";

return await connection.QueryAsync<T>(
query,
param: parameters,
commandTimeout: commandTimeout,
transaction: transaction
);
}

private static string CreateTemporaryTableFromTable(IDbConnection connection, string tableName, IEnumerable<PropertyInfo> 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<T>(
Expand Down

0 comments on commit a5cd17b

Please sign in to comment.