C# SQL Bulk Copy

发布于:2025-07-03 ⋅ 阅读:(18) ⋅ 点赞:(0)

本篇文章用于介绍怎么使用SQL Bulk Copy去提高批量插入数据的性能。

This function is to handle hundreds/thousands inserting of Entities, it has higher performance.

Currently it works only when C# properties match with DB columns names.

引入using Microsoft.Data.SqlClient;

目录

主代码

TableBase.cs

IAdoTable.cs

 IAdoEntity.cs

Attributes


主代码

        #region SqlBulkCopy

        /// <summary>
        /// This function is to handle hundreds/thousands inserting of Entities, it has higher performance.
        /// Currently it works only when C# properties match with DB columns names.
        /// </summary>
        public static void BulkInsertBySqlBulkCopyFromEntities<T>(List<T> adoEntities) where T : EntityBase, IAdoEntity, new()
        {
            if (!adoEntities.Any()) return;

            var mainDataTable = ConvertAdoEntitiesToDataTable(adoEntities);

            TableBase<T> tableBase = new TableBase<T>();
            var mainTableName = tableBase.GetDbTableName();
            var connectionString = ConfigurationManager.ConnectionStrings["Default"].ConnectionString;

            TransactionOptions option = new TransactionOptions()
            {
                IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead,
                Timeout = TimeSpan.FromMinutes(20)
            };
            using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, option))
            {
                // Insert Associate AuditTrail
                DataTable auditTrailDataTable = new DataTable();

                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    using (SqlBulkCopy sqlbulkCopy = new SqlBulkCopy(connectionString, SqlBulkCopyOptions.UseInternalTransaction))
                    {
                        // Insert Main table Data
                        conn.Open();
                        var command = new SqlCommand($"SELECT IDENT_CURRENT('{mainTableName}');", conn);
                        var identityId = command.ExecuteScalar();
                        conn.Close();
                        BulkInsertBySqlBulkCopyFromDataTable(mainDataTable, mainTableName, sqlbulkCopy);
                        
                    }

                }

                scope.Complete();
            }
        }

        internal static void BulkInsertBySqlBulkCopyFromDataTable(DataTable dataTable, string tableName, SqlBulkCopy sqlbulkCopy)
        {
            sqlbulkCopy.DestinationTableName = tableName;
            sqlbulkCopy.ColumnMappings.Clear();
            for (int i = 0; i < dataTable.Columns.Count; i++)
            {
                sqlbulkCopy.ColumnMappings.Add(dataTable.Columns[i].ColumnName, dataTable.Columns[i].ColumnName);
            }

            sqlbulkCopy.WriteToServer(dataTable);
        }

        internal static DataTable ConvertAdoEntitiesToDataTable<T>(List<T> adoEntities) where T : EntityBase, IAdoEntity, new()
        {
            if (!adoEntities.Any()) return null;

            DataTable dataTable = new DataTable();

            adoEntities.First().GenerateDataTableColumns(dataTable);
            adoEntities.ForEach(adoEntity =>
            {
                adoEntity.SetForeignKeys();
                DataRow adoEntityRow = adoEntity.MapToDataRow(dataTable);
                dataTable.Rows.Add(adoEntityRow);
            });

            return dataTable;
        }

        #endregion

TableBase.cs

[DbTableConnectionString("Default")]
public class TableBase<T> : IAdoTable<T> where T : class, IAdoEntity, new()
{
    #region Members

    protected T Entity;
    T IAdoTable<T>.Entity { get => Entity; set => Entity = value; }
    IAdoEntity IAdoTable.Entity { get => Entity; set => Entity = (T)value; }
    public string QueryString { get; set; }
    public string Column { get; set; }
    private Boolean? _isRowVersion { get; set; }
    public Boolean IsRowVersionExist
    {
        get
        {
            if (_isRowVersion.HasValue == false)
            {
                _isRowVersion = Entity.GetType()
                    .GetProperty(SqlConstants.Columns.RowVersion) != null;
            }
            return this._isRowVersion.Value;
        }
    }

    #endregion


    #region Attributes

    public string GetDbTableName()
    {
        DbTableNameAttribute tableNameAttribute;

        tableNameAttribute =
            typeof(T).GetCustomAttributes(typeof(DbTableNameAttribute), true).FirstOrDefault() as DbTableNameAttribute;

        if (tableNameAttribute == null)
        {
            return string.Empty;
        }

        return tableNameAttribute.Value;
    }

    #endregion


    #region SQL Stored Procedures

    public string SELECT_COUNT
    {
        get
        {
            if (!string.IsNullOrEmpty(this.QueryString))
            {
                return string.Format(SqlConstants.StatementFormatting.SELECT_COUNT_FROM_WHERE, this.GetDbTableName() + " (nolock) ", this.QueryString);
            }

            return string.Format(SqlConstants.StatementFormatting.SELECT_COUNT_FROM, this.GetDbTableName() + " (nolock) ");
        }
    }

    public string SELECT_COLUMN
    {
        get
        {
            if (!string.IsNullOrEmpty(this.QueryString))
            {
                return string.Format(SqlConstants.StatementFormatting.SELECT_COLUMN_FROM_WHERE, this.Column, this.GetDbTableName(), this.QueryString);
            }

            return string.Format(SqlConstants.StatementFormatting.SELECT_COLUMN_FROM, this.Column, this.GetDbTableName());
        }
    }

    public string SELECT
    {
        get
        {
            if (!string.IsNullOrEmpty(this.QueryString))
            {
                return string.Format(SqlConstants.StatementFormatting.SELECT_FROM_WHERE, GetDbTableName(), this.QueryString);
            }

            return string.Format(SqlConstants.StatementFormatting.SELECT_FROM, GetDbTableName());
        }
    }

    protected virtual void AddIdentitySqlParameter(SqlParameterCollection parameters)
    {
        SqlParameter identityParam = new SqlParameter();
        identityParam.ParameterName = string.Format("@{0}", SqlConstants.Columns.IDENTITY);
        identityParam.Size = 10;
        identityParam.Direction = ParameterDirection.Output;

        parameters.Add(identityParam);
    }

    protected virtual void AddInsertSqlParameters(SqlParameterCollection parameters)
    {
        //this.AddIdentitySqlParameter(parameters);

        this.AddDefaultSqlParameters(parameters);
    }

    protected virtual void AddUpdateSqlParameters(SqlParameterCollection parameters)
    {
        this.AddDefaultSqlParameters(parameters);

        if (!parameters.Contains(string.Format("@{0}", SqlConstants.Columns.IDENTITY)))
            parameters.AddWithValue(string.Format("@{0}", SqlConstants.Columns.IDENTITY), this.Entity.Id);
    }

    protected virtual void AddDeleteSqlParameters(SqlParameterCollection parameters)
    {
        parameters.AddWithValue(string.Format("@{0}", SqlConstants.Columns.IDENTITY), this.Entity.Id);
        if (this.IsRowVersionExist)
        {
            PropertyInfo rowversionProp = this.Entity.GetType().GetProperties()
                .Single(e => e.Name == SqlConstants.Columns.RowVersion);
            parameters.AddWithValue(string.Format("@{0}", SqlConstants.Columns.RowVersion), rowversionProp.GetValue(this.Entity));
        }
    }

    public virtual void AddDefaultSqlParameters(SqlParameterCollection parameters)
    {
        T entity = this.Entity as T;

        PropertyInfo[] propertyInfos = entity.GetType().GetProperties();

        foreach (PropertyInfo propInfo in propertyInfos)
        {
            if (propInfo.IsDefined(typeof(DbIgnoreAttribute), true) || propInfo.IsDefined(typeof(DbReadOnlyAttribute), true))
            {
                continue;
            }

            string propertyName = propInfo.IsDefined(typeof(DbColumnNameAttribute), true)
                ? ((DbColumnNameAttribute)propInfo.GetCustomAttributes(typeof(DbColumnNameAttribute), true).First()).Value
                : propInfo.Name;

            if (propInfo.IsDefined(typeof(DbIgnoreAttribute), true) ||
                    propertyName == SqlConstants.Columns.IDENTITY && !propInfo.IsDefined(typeof(DbCompositeKeyAttribute), true))
            {
                continue;
            }

            object propertyValue = propInfo.GetValue(entity, null);

            if (propertyValue == null)
            {
                if (propInfo.PropertyType == typeof(string))
                    propertyValue = string.Empty;
                else
                    propertyValue = DBNull.Value;
            }

            if (propInfo.IsDefined(typeof(UTCField), true) && propertyValue != DBNull.Value)
            {
                propertyValue = ((DateTime)propertyValue).ToUniversalTime();
            }

            SqlParameter parameter = new SqlParameter($"@{propertyName}", propertyValue);     // Create Parameter.
            if (parameter.SqlDbType == SqlDbType.NVarChar) parameter.Size = -1;
            if (propInfo.PropertyType.IsEnum)
            {
                parameter.SqlDbType = SqlDbType.NVarChar;
                parameter.Size = -1;
            }

            try
            {
                var value = parameter.SqlValue;     // Evaluate parameter value. If value is invalid, SqlValue will throw an exception.
                // Running queries with bad parameter value abruptly terminates TransactionScope() causing partial information save in the system.
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Invalid value for parameter '{0}: {1}' - {2}", propertyName, propertyValue, ex.Message));   // Throw custom exception with parameter details and errors.
            }

            parameters.Add(parameter);  // Add parameter to parameters collections.
        }
    }

    #endregion


    #region SQL Statements

    public string GetRowVersionStatement()
    {
        StringBuilder sb = new StringBuilder();
        string selectColumncommand = string.Format(SqlConstants.StatementFormatting.SELECT_COLUMN_FROM
            , SqlConstants.Columns.RowVersion
            , this.GetDbTableName());
        sb.Append(selectColumncommand);
        sb.Append(' ');
        sb.Append(string.Format(SqlConstants.StatementFormatting.WHERE_COLUMN_EQUALS_PARAMETER
            , SqlConstants.Columns.IDENTITY
            , SqlConstants.Columns.IDENTITY
            ));
        return sb.ToString();
    }

    public string GetSqlStatement(SqlCommandTextEnum sqlCommandTextEnum)
    {
        StringBuilder sb = new StringBuilder();

        sb.Append(this.GetSqlStatementAction(sqlCommandTextEnum));

        switch (sqlCommandTextEnum)
        {
            case SqlCommandTextEnum.DELETE:
                {
                    sb.Append(this.GetSqlStatementIdentityWhere());

                    break;
                }
            case SqlCommandTextEnum.INSERT:
                {
                    sb.Append(this.GetSqlStatementInsertColumnsAndParameters());
                    sb.Append(this.GetSqlStatementIdentityScope());

                    break;
                }
            case SqlCommandTextEnum.UPDATE:
                {
                    sb.Append(this.GetSqlStatementUpdateSetParameters());
                    sb.Append(this.GetSqlStatementIdentityWhere());

                    break;
                }
            default:
                {
                    return string.Empty;
                }
        }

        return sb.ToString();
    }

    #endregion


    #region SQL Helpers

    private string GetSqlStatementIdentityScope()
    {
        StringBuilder sb = new StringBuilder();
        if (this.IsRowVersionExist)
        {
            sb.Append(SqlConstants.StatementAddons.RETURN_SCOPE_IDENTITY_AND_ROWVERSION);
        }
        else
        {
            sb.Append(SqlConstants.StatementAddons.RETURN_SCOPE_IDENTITY);
        }

        return sb.ToString();
    }

    private string GetSqlStatementUpdateSetParameters()
    {
        if (this.Entity == null)
        {
            return string.Empty;
        }

        StringBuilder sb = new StringBuilder();

        sb.Append(" SET ");

        T entity = this.Entity as T;

        PropertyInfo[] propertyInfos = entity.GetType().GetProperties();

        foreach (PropertyInfo propInfo in propertyInfos)
        {
            string propertyName = propInfo.IsDefined(typeof(DbColumnNameAttribute), true)
                ? ((DbColumnNameAttribute)propInfo.GetCustomAttributes(typeof(DbColumnNameAttribute), true).First()).Value
                : propInfo.Name;

            if (propInfo.IsDefined(typeof(DbIgnoreAttribute), true) || propInfo.IsDefined(typeof(DbReadOnlyAttribute), true) ||
                    (propertyName == SqlConstants.Columns.IDENTITY && !propInfo.IsDefined(typeof(DbCompositeKeyAttribute), true))
                    || propertyName == SqlConstants.Columns.RowVersion)
            {
                continue;
            }

            sb.Append(string.Format(SqlConstants.StatementFormatting.COLUMN_EQUALS_PARAMETER_COMMA, propertyName, propertyName));
        }

        sb.Remove(sb.Length - 2, 2);

        sb.Append(' ');

        return sb.ToString();
    }

    private string GetSqlStatementIdentityWhere()
    {
        if (this.Entity == null)
        {
            return string.Empty;
        }

        StringBuilder sb = new StringBuilder();

        sb.Append(' ');
        if (this.IsRowVersionExist)
        {
            sb.Append(string
                .Format(SqlConstants.StatementFormatting.WHERE_TWO_COLUMNS_EQUALS_PARAMETER
                , SqlConstants.Columns.IDENTITY
                , SqlConstants.Columns.IDENTITY
                , SqlConstants.Columns.RowVersion
                , SqlConstants.Columns.RowVersion));
        }
        else
        {
            sb.Append(string.Format(SqlConstants.StatementFormatting.WHERE_COLUMN_EQUALS_PARAMETER, SqlConstants.Columns.IDENTITY, SqlConstants.Columns.IDENTITY));
        }


        return sb.ToString();
    }

    private string GetSqlStatementInsertColumnsAndParameters()
    {
        if (this.Entity == null)
        {
            return string.Empty;
        }

        StringBuilder sbInsert = new StringBuilder();
        StringBuilder sbValues = new StringBuilder();

        sbInsert.Append("( ");
        sbValues.Append("VALUES (");

        T entity = this.Entity as T;

        PropertyInfo[] properties = entity.GetType().GetProperties();

        foreach (PropertyInfo property in properties)
        {
            string propertyName = property.IsDefined(typeof(DbColumnNameAttribute), true)
                ? ((DbColumnNameAttribute)property.GetCustomAttributes(typeof(DbColumnNameAttribute), true).First()).Value
                : property.Name;

            if (property.IsDefined(typeof(DbIgnoreAttribute), true) || property.IsDefined(typeof(DbReadOnlyAttribute), true) ||
                    (propertyName == SqlConstants.Columns.IDENTITY && !property.IsDefined(typeof(DbCompositeKeyAttribute), true))
                    || propertyName == SqlConstants.Columns.RowVersion)
            {
                continue;
            }

            sbInsert.Append(string.Format(SqlConstants.StatementFormatting.INSERT_COLUMN, propertyName));
            sbValues.Append(string.Format(SqlConstants.StatementFormatting.PARAMETER_COMMA, propertyName));
        }

        sbInsert.Remove(sbInsert.Length - 2, 2);
        sbValues.Remove(sbValues.Length - 2, 2);

        sbInsert.Append(')');
        sbValues.Append(')');

        return string.Format("{0} {1}", sbInsert.ToString(), sbValues.ToString());
    }

    private string GetSqlStatementAction(SqlCommandTextEnum sqlCommandTextEnum)
    {
        StringBuilder sb = new StringBuilder();

        switch (sqlCommandTextEnum)
        {
            case SqlCommandTextEnum.INSERT:
                {
                    sb.Append(string.Format(SqlConstants.Actions.INSERT_INTO, this.GetDbTableName()));

                    return sb.ToString();
                }
            case SqlCommandTextEnum.UPDATE:
                {
                    sb.Append(string.Format(SqlConstants.Actions.UPDATE, this.GetDbTableName()));

                    return sb.ToString();
                }
            case SqlCommandTextEnum.DELETE:
                {
                    sb.Append(string.Format(SqlConstants.Actions.DELETE_FROM, this.GetDbTableName()));

                    return sb.ToString();
                }
            default:
                {
                    return string.Empty;
                }
        }
    }

    public void GetSqlCommandParams(SqlParameterCollection parameters, SqlCommandTextEnum sqlCommandTextEnum)
    {
        switch (sqlCommandTextEnum)
        {
            case SqlCommandTextEnum.INSERT:
                {
                    AddInsertSqlParameters(parameters);

                    break;
                }
            case SqlCommandTextEnum.UPDATE:
                {
                    AddUpdateSqlParameters(parameters);

                    break;
                }
            case SqlCommandTextEnum.DELETE:
                {
                    AddDeleteSqlParameters(parameters);

                    break;
                }
        }
    }

    #endregion


    #region SQL DataSet Mapping

    public virtual T MapRow(SqlDataReader reader)
    {
        T entity = new T();

        entity.Map(reader);

        return entity;
    }

    IAdoEntity IAdoTable.MapRow(SqlDataReader reader)
    {
        return MapRow(reader);
    }

    #endregion


    #region Ctor

    public TableBase()
    {

    }

    public TableBase(T entity)
    {
        this.Entity = entity;
    }

    #endregion


    #region Common Parameter

    public static void GetReadSqlParameterDateRange(SqlParameterCollection parameters, object entity)
    {
        parameters.AddWithValue("@UserId", ((dynamic)entity).UserId);
        parameters.AddWithValue("@StartDate", ((dynamic)entity).StartDate);
        parameters.AddWithValue("@EndDate", ((dynamic)entity).EndDate);
        parameters.AddWithValue("@CutOffDate", ((dynamic)entity).CutOffDate);
    }

    public static void GetReadReconCommonSqlParameters(SqlParameterCollection parameters, object entity)
    {
        parameters.AddWithValue("@UserId", ((dynamic)entity).UserId);
        parameters.AddWithValue("@StartDate", ((dynamic)entity).StartDate);
        parameters.AddWithValue("@EndDate", ((dynamic)entity).EndDate);
        parameters.AddWithValue("@CutOffDate", ((dynamic)entity).CutOffDate);
        parameters.AddWithValue("@IncludeFinalizedRecord", ((dynamic)entity).IncludeFinalizedRecord);
    }

    public static void GetReadCommonByUserIdSqlParameters(SqlParameterCollection parameters, object entity)
    {
        int userId = ((ApplicationUser)entity).Id;

        parameters.AddWithValue("@UserId", userId);
    }

    public static void GetByUserIdDateRangeSqlParameters(SqlParameterCollection parameters, object entity)
    {
        var entityBase = entity as EntityBase;

        parameters.AddWithValue("@UserId", ((dynamic)entityBase).UserId);
        parameters.AddWithValue("@StartDate", ((dynamic)entityBase).StartDate);
        parameters.AddWithValue("@EndDate", ((dynamic)entityBase).EndDate);
    }

    public static void GetByDateRangeSqlParameters(SqlParameterCollection parameters, object entity)
    {
        var entityBase = entity as EntityBase;

        parameters.AddWithValue("@StartDate", ((dynamic)entityBase).StartDate);
        parameters.AddWithValue("@EndDate", ((dynamic)entityBase).EndDate);
    }

    #endregion
}

IAdoTable.cs

public interface IAdoTable<T> : IAdoTable where T : class, IAdoEntity, new()
{
    new T MapRow(SqlDataReader reader);

    new T Entity { get; set; }
}

 IAdoEntity.cs

public interface IAdoEntity
{
    int Id { get; set; }
    int GetDbPriority();
    int GetDeletePriority();
    bool IsUpdateAdoHash { get; }

    string AdoHash { get; set; }
    string AdoCacheKey { get; set; }

    void CascadeDelete();
    void Map(SqlDataReader row);
    void GenerateDataTableColumns(DataTable dataTable);
    DataRow MapToDataRow(DataTable dataTable);
    void RaisePropertyChangedEvent(string propertyName);
    void SetForeignKeys();
    void SetCompositeKeys();

    AdoEntityStateEnum State { get; set; }

    object Clone();

    bool IsBulkEntity { get; set; }
}

Attributes

 public sealed class DbTableConnectionStringAttribute : Attribute
 {
     private string _value;
     public string Value
     {
         get { return _value; }
         set { _value = value; }
     }

     public DbTableConnectionStringAttribute(string connectionStringName)
     {
         this.Value = connectionStringName;
     }
 }
    public sealed class DbTableNameAttribute : System.Attribute
    {
        private string _value;
        public string Value
        {
            get { return _value; }
            set
            {
                _value = value;
            }
        }

        public DbTableNameAttribute(string table)
        {
            this.Value = table;
        }
    }