Files
Umbraco-CMS/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs

122 lines
6.0 KiB
C#
Raw Normal View History

2017-05-12 14:49:44 +02:00
using System;
using System.Collections.Generic;
using System.Data;
2021-10-29 10:14:52 +02:00
using System.Data.Common;
2017-05-12 14:49:44 +02:00
using System.Linq;
2022-01-18 15:23:53 +00:00
using Microsoft.Data.SqlClient;
2017-05-12 14:49:44 +02:00
using NPoco;
2022-01-18 15:23:53 +00:00
using NPoco.SqlServer;
2021-10-29 10:14:52 +02:00
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence;
2017-05-12 14:49:44 +02:00
namespace Umbraco.Extensions
2017-05-12 14:49:44 +02:00
{
/// <summary>
2021-10-29 10:14:52 +02:00
/// Provides extension methods to NPoco Database class.
2017-05-12 14:49:44 +02:00
/// </summary>
public static partial class NPocoDatabaseExtensions
{
/// <summary>
2021-10-29 10:14:52 +02:00
/// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the
/// underlying RetryDbConnection and ProfiledDbTransaction
/// </summary>
/// <remarks>
2021-10-29 10:14:52 +02:00
/// This is required to use NPoco's own <see cref="Database.InsertBulk{T}(IEnumerable{T})" /> method because we use
/// wrapped DbConnection and DbTransaction instances.
/// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for
/// bulk inserting of records for
/// any other database type and in which case will just insert records one at a time.
/// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own
/// BulkInsertRecords methods
/// do not handle this scenario.
/// </remarks>
public static void ConfigureNPocoBulkExtensions()
{
SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection<SqlConnection>(dbConn);
SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction<SqlTransaction>(dbTran);
}
2017-05-12 14:49:44 +02:00
/// <summary>
2021-10-29 10:14:52 +02:00
/// Creates bulk-insert commands.
2017-05-12 14:49:44 +02:00
/// </summary>
/// <typeparam name="T">The type of the records.</typeparam>
/// <param name="database">The database.</param>
/// <param name="records">The records.</param>
/// <returns>The sql commands to execute.</returns>
internal static IDbCommand[] GenerateBulkInsertCommands<T>(this IUmbracoDatabase database, T[] records)
{
2021-10-29 10:14:52 +02:00
if (database?.Connection == null)
{
throw new ArgumentException("Null database?.connection.", nameof(database));
}
2017-05-12 14:49:44 +02:00
2021-10-29 10:14:52 +02:00
PocoData pocoData = database.PocoDataFactory.ForType(typeof(T));
2017-05-12 14:49:44 +02:00
// get columns to include, = number of parameters per row
2021-10-29 10:14:52 +02:00
KeyValuePair<string, PocoColumn>[] columns =
pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray();
2017-05-12 14:49:44 +02:00
var paramsPerRecord = columns.Length;
// format columns to sql
var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName);
2021-10-29 10:14:52 +02:00
var columnNames = string.Join(", ",
columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key)));
2017-05-12 14:49:44 +02:00
// example:
// assume 4168 records, each record containing 8 fields, ie 8 command parameters
// max 2100 parameter per command
// Math.Floor(2100 / 8) = 262 record per command
// 4168 / 262 = 15.908... = there will be 16 command in total
// (if we have disabled db parameters, then all records will be included, in only one command)
2021-10-29 10:14:52 +02:00
var recordsPerCommand = paramsPerRecord == 0
? int.MaxValue
: Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord));
2017-05-12 14:49:44 +02:00
var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand));
var commands = new IDbCommand[commandsCount];
var recordsIndex = 0;
var recordsLeftToInsert = records.Length;
var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString);
for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++)
{
2021-10-29 10:14:52 +02:00
DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty);
2017-05-12 14:49:44 +02:00
var parameterIndex = 0;
var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert);
var recordsValues = new string[commandRecords];
2021-10-29 10:14:52 +02:00
for (var commandRecordIndex = 0;
commandRecordIndex < commandRecords;
commandRecordIndex++, recordsIndex++, recordsLeftToInsert--)
2017-05-12 14:49:44 +02:00
{
2021-10-29 10:14:52 +02:00
T record = records[recordsIndex];
2017-05-12 14:49:44 +02:00
var recordValues = new string[columns.Length];
for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++)
{
database.AddParameter(command, columns[columnIndex].Value.GetValue(record));
recordValues[columnIndex] = prefix + parameterIndex++;
}
2021-10-29 10:14:52 +02:00
2017-05-12 14:49:44 +02:00
recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")";
}
2021-10-29 10:14:52 +02:00
command.CommandText =
$"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}";
2017-05-12 14:49:44 +02:00
commands[commandIndex] = command;
}
return commands;
}
/// <summary>
2021-10-29 10:14:52 +02:00
/// Determines whether a column should be part of a bulk-insert.
2017-05-12 14:49:44 +02:00
/// </summary>
/// <param name="pocoData">The PocoData object corresponding to the record's type.</param>
/// <param name="column">The column.</param>
/// <returns>A value indicating whether the column should be part of the bulk-insert.</returns>
/// <remarks>Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts.</remarks>
2021-10-29 10:14:52 +02:00
public static bool IncludeColumn(PocoData pocoData, KeyValuePair<string, PocoColumn> column) =>
column.Value.ResultColumn == false
&& (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey);
2017-05-12 14:49:44 +02:00
}
}