GitHub上項目地址:https://github.com/shendashan/BulkCopy
最近在工作中遇到一些性能問題,在大批量的數據寫入和修改數據庫時太慢了,甚至會出現操作超時。
所以去網上找了下資料,找到了一些解決方案SqlBulkCopy和SqlDataAdapter(SqlDataAdapter實測了下,批量修改數據的時候速度不快,可能是我使用的姿勢不對。哪位大神知道正確使用姿勢,望留言指點)。下面主要介紹下SqlBulkCopy:
SqlBulkCopy
SqlBulkCopy用於做批量寫入的操作,通過調用WriteToServer方法來實現批量寫入的功能(WriteToServer方法的實現原理沒有深入研究)。從測試結果來看,執行效率完全可以滿足公司的需求。
壹.先來說下批量寫入的具體實現:
1.在調用WriteToServer方法之前,需要准備一個DataTable實例,DataTable的實例中存放的是需要批量寫入的數據集。
如圖,先創建一個生成DataTable的方法:

第一個參數,需要批量寫入的集合,這里使用的泛型集合,以方便共用。
第二個參數,SqlConnection對象,用於連接數據庫。
第三個參數,被寫入目標表的名字,因為是一個共用的方法,不然無法知道要向哪張表寫數據。
方法內部實現:
創建一條查詢的sql語句,如圖:

上面圖中的sql語句,使用來獲取目標表的字段。通過SqlCommand的ExecuteReader方法來獲取SqlDataReader的對象。然后在通過獲取到的SqlDataReader對象來獲取表的列名和對應的類型。
拿到列名和類型后,使用DataColumn的有參構造函數創建DataColumn的對象(如:column),將對象(column)添加到DataTable對象的Columns屬性中。如圖:

到這里,要寫入目標表的所以列都已經添加到DataTable中了(別忘記關閉DataReader,不然運行的時候回報錯哦O(∩_∩)O),下面就可以去處理要批量寫入到表的集合數據了。
在處理集合數據之前,先利用反射技術獲取到實體的所有屬性。首先通過typeof()獲取到Type對象,然后通過對象的GetProperties()來獲取所有屬性的數組,如圖:

獲取到了實體的所有屬性,接下來就可以處理要寫入的數據集合了。
循環數據集合,對數據進行逐條處理。在循環的內部通過循環實體屬性的數組對象來獲取對應的數據。在根據屬性獲取數據值的時候,需要進一步處理。因為實體屬性的數組中很有可能包含導航屬性(導航屬性:ef中codefirst建表時,建立表之間關聯關系的屬性),如果不處理掉導航屬性的話,執行語句的時候會出問題。將處理之后的屬性和獲取到的值添加到DataRow中。如下圖:

寫到這里,DataTable的對象就已經准備好了,要寫人的數據也已經添加到DataTable的對象中了。
接下來就使用SqlBulkCopy來批量將數據寫入到數據庫中。這一步很簡單,只要創建一個SqlBulkCopy的對象,告訴SqlBulkCopy被寫入表的名字,然后調用WriteToServer()將准備好的DataTable對象放到方法中就OK了。如下圖:

批量寫入數據的方法到這里就完全結束了。最后別忘了將SqlConnection對象關閉。
具體代碼如下:
a)批量插入數據代碼片段
/// <summary> /// 批量插入數據 /// </summary> /// <typeparam name="TModel"></typeparam> /// <param name="modelList">數據集合</param> /// <param name="connectionString">數據庫連接字符串</param> /// <param name="tableName">表名</param> public static void BulkInert<TModel>(IList<TModel> modelList, string connectionString, string tableName) { try { using (SqlConnection sqlConnect = new SqlConnection(connectionString)) { DataTable dt = ToSqlBulkCopyDataTable(modelList, sqlConnect, tableName); SqlBulkCopy sqlBulk = null; sqlBulk = new SqlBulkCopy(sqlConnect); using (sqlBulk) { sqlBulk.DestinationTableName = tableName; if (sqlConnect.State != ConnectionState.Open) { sqlConnect.Open(); } sqlBulk.WriteToServer(dt); } } } catch (Exception ex) { throw ex; } }
b)生成DataTable對象代碼片段
/// <summary> /// 生成DataTable /// </summary> /// <typeparam name="TModel"></typeparam> /// <param name="modelList">數據集合</param> /// <param name="conn">SqlConnection對象</param> /// <param name="tableName">表名</param> /// <returns></returns> private static DataTable ToSqlBulkCopyDataTable<TModel>(IList<TModel> modelList, SqlConnection conn, string tableName) { DataTable dt = new DataTable(); #region 獲取所有表字段 string sql = string.Format("select top 0 * from {0}", tableName); if (conn.State != ConnectionState.Open) { conn.Open(); } SqlCommand command = new SqlCommand(); command.CommandText = sql; command.CommandType = CommandType.Text; command.Connection = conn; SqlDataReader reader = command.ExecuteReader(); try { for (int i = 0; i < reader.FieldCount; i++) { var re_name = reader.GetName(i); var re_type = reader.GetFieldType(i); DataColumn column = new DataColumn(re_name, re_type); dt.Columns.Add(column); } } catch (Exception ex) { throw ex; } finally { if (reader != null) { reader.Close(); } } #endregion //獲取實體 Type mType = typeof(TModel); var mType_Properts = mType.GetProperties(); foreach (var model in modelList) { DataRow dr = dt.NewRow(); foreach (var proper in mType_Properts) { string fullName = proper.PropertyType.FullName; bool isValueType = proper.PropertyType.IsValueType; bool isClass = proper.PropertyType.IsClass; bool isEnum = proper.PropertyType.IsEnum; if ((isValueType || isEnum) && !isClass) { object value = proper.GetValue(model); if (proper.PropertyType.IsEnum) { if (value != null) { value = (int)value; } } dr[proper.Name] = value ?? DBNull.Value; } else if (fullName == "System.String") { object value = proper.GetValue(model); dr[proper.Name] = value ?? DBNull.Value; } } dt.Rows.Add(dr); } return dt; }
到這里批量寫入就結束了O(∩_∩)O
貳.接下來要說的就是批量修改了
為了能更快的更新數據,在批量修改的實現中還需要用到SqlBulkCopy。這里實現批量修改的操作很簡單,只要在批量寫入的操作之后再執行批量修改的操作就行了(方法比較笨^_^)。
下面開始正式介紹批量修改的實現:
1.首先要准備一個臨時表,用於保存要修改的數據(注意:臨時表的名字不可重復,不然會報錯,這里要做處理,保證表名的唯一性)。
2.使用SqlBulkCopy來向臨時表 中寫入數據,以確保數據能快速寫入到臨時表中。
3.同樣,使用反射技術來獲取到修改的實體屬性,然后根據獲取到的屬性來組建update語句,用update語句將臨時表和被修改的目標表關聯起來進線批量修改操作。
批量修改的操作就這么簡單的幾個步驟。需要注意的地方就是在組建update語句的時候對實體屬性的處理,一定要將不屬於表的導航屬性去除掉。不然在執行修改的時候會報出錯誤“XXX列不存在”的提示。下面之間貼出代碼:
a)批量修改代碼片段
/// <summary> /// 批量修改數據 /// </summary> /// <param name="modelList"></param> /// <param name="connectionString">數據庫連接字符串</param> /// <param name="tableName">表名</param> /// <param name="primaryKey">主鍵</param> /// <returns></returns> public static int BulkUpdate<TModel>(IList<TModel> modelList, string connectionString, string tableName, string primaryKey) { try { Debug.WriteLine("進入BulkCopy"); //臨時表名使用日期加隨機數 Random ran = new Random(); int ranNumber = ran.Next(1, 10000); string dateStr = DateTime.Now.ToString("yyyyMMddHHmmss"); string tempName = "#" + tableName + dateStr + ranNumber; var model = typeof(TModel); var propers = model.GetProperties(); StringBuilder updateStrBuild = new StringBuilder(); foreach (var item in propers) { string fullName = item.PropertyType.FullName; bool isValueType = item.PropertyType.IsValueType; bool isClass = item.PropertyType.IsClass; bool isEnum = item.PropertyType.IsEnum; if ((isValueType || isEnum) && !isClass) { updateStrBuild.Append(" t2." + item.Name + " = t1." + item.Name + ","); } else if (fullName == "System.String") { updateStrBuild.Append(" t2." + item.Name + " = t1." + item.Name + ","); } } string updaSql = updateStrBuild.ToString(); updaSql = updaSql.TrimEnd(','); Debug.WriteLine("修改語句" + updaSql); string updateSql = string.Format("update t2 SET {2} FROM {0} AS t1,{1} AS t2 WHERE t1.{3} = t2.{3}", tempName, tableName, updaSql, primaryKey); Debug.WriteLine(updateSql); StringBuilder strB = new StringBuilder(); foreach (var item in propers) { string fullName = item.PropertyType.FullName; bool isValueType = item.PropertyType.IsValueType; bool isClass = item.PropertyType.IsClass; bool isEnum = item.PropertyType.IsEnum; if ((isValueType || isEnum) && !isClass) { strB.Append(" " + item.Name + ", "); } else if (fullName == "System.String") { strB.Append(" " + item.Name + ", "); } } strB.Append(" " + primaryKey + " as Ids "); string strSql = strB.ToString(); using (SqlConnection conn = new SqlConnection(connectionString)) { string sql = "SELECT top 0 " + strSql + " into " + tempName + " from " + tableName; if (conn.State != ConnectionState.Open) { conn.Open(); } SqlCommand command = new SqlCommand(); command.CommandText = sql; command.CommandType = CommandType.Text; command.Connection = conn; command.ExecuteNonQuery(); DataTable dt = ToSqlBulkCopyDataTable2(modelList, conn, tableName, strSql, primaryKey); SqlBulkCopy sqlBulk = null; sqlBulk = new SqlBulkCopy(conn); using (sqlBulk) { sqlBulk.DestinationTableName = tempName; if (conn.State != ConnectionState.Open) { conn.Open(); } sqlBulk.WriteToServer(dt); } command.CommandText = updateSql; int count = command.ExecuteNonQuery(); return count; } } catch (Exception ex) { throw ex; } }
b)批量修改生成DataTable對象的代碼片段
/// <summary> /// 批量修改准備DataTable /// </summary> /// <typeparam name="TModel"></typeparam> /// <param name="modelList"></param> /// <param name="conn"></param> /// <param name="tableName"></param> /// <param name="strSql"></param> /// <param name="key"></param> /// <returns></returns> private static DataTable ToSqlBulkCopyDataTable2<TModel>(IList<TModel> modelList, SqlConnection conn, string tableName, string strSql, string key) { Debug.WriteLine("進入ToSqlBulkCopyDataTable2"); DataTable dt = new DataTable(); //獲取實體 Type mType = typeof(TModel); var mType_Properts = mType.GetProperties(); #region string sql = string.Format("select top 1 " + strSql + " from {0}", tableName); Debug.WriteLine("查詢語句:" + sql); if (conn.State != ConnectionState.Open) { conn.Open(); } SqlCommand command = new SqlCommand(); command.CommandText = sql; command.CommandType = CommandType.Text; command.Connection = conn; var reader = command.ExecuteReader(); try { while (reader.Read()) { for (int i = 0; i < reader.FieldCount; i++) { var re_name = reader.GetName(i); var re_type = reader.GetFieldType(i); DataColumn column = new DataColumn(re_name, re_type); dt.Columns.Add(column); } } } catch (Exception ex) { throw ex; } finally { if (reader != null) { reader.Close(); } } #endregion foreach (var model in modelList) { DataRow dr = dt.NewRow(); foreach (var proper in mType_Properts) { string fullName = proper.PropertyType.FullName; bool isValueType = proper.PropertyType.IsValueType; bool isClass = proper.PropertyType.IsClass; bool isEnum = proper.PropertyType.IsEnum; if ((isValueType || isEnum) && !isClass) { object value = proper.GetValue(model); if (proper.PropertyType.IsEnum) { if (value != null) { value = (int)value; } } dr[proper.Name] = value ?? DBNull.Value; if (key.Equals(proper.Name)) { dr["Ids"] = value; } } else if (fullName == "System.String") { object value = proper.GetValue(model); dr[proper.Name] = value ?? DBNull.Value; if (key.Equals(proper.Name)) { dr["Ids"] = value; } } } dt.Rows.Add(dr); } return dt; }
對數據的批量修改和批量寫入完全結束了O(∩_∩)O
