C# 如何将通用源数组批量插入任意SQL表
我的应用程序使用实体框架。大多数时候我对此感觉很好,但当我需要进行批量插入时,我遇到了麻烦。我还没有找到一种方法让EF快速完成这些任务。因此,我需要一个超越EF的解决方案来进行更新 我想要一个方法,它接受连接字符串、目标表名和源数据的通用数组,并执行大容量插入。此外,我希望它将源数据的属性映射到特定的表字段,理想情况下不需要源对象中的属性来指定表字段 因此,对于这个目标:C# 如何将通用源数组批量插入任意SQL表,c#,sql,sql-server,entity-framework,generics,C#,Sql,Sql Server,Entity Framework,Generics,我的应用程序使用实体框架。大多数时候我对此感觉很好,但当我需要进行批量插入时,我遇到了麻烦。我还没有找到一种方法让EF快速完成这些任务。因此,我需要一个超越EF的解决方案来进行更新 我想要一个方法,它接受连接字符串、目标表名和源数据的通用数组,并执行大容量插入。此外,我希望它将源数据的属性映射到特定的表字段,理想情况下不需要源对象中的属性来指定表字段 因此,对于这个目标: public class Customer { //property populated & used on
public class Customer
{
//property populated & used only in memory
public string TempProperty { get; set; }
//properties saved in the database
public string Name { get; set; }
public string Address { get; set; }
public int Age { get; set; }
public string Comments { get; set; }
//various methods, constructors, etc.
}
我应该能够提供表名
Data.Customer
,方法应该映射Customer.name->Data.Customers.name
,Customer.Address->Data.Customers.Address
,等等。我很惊讶我在任何地方都找不到类似的代码。也许这不是解决问题的最好办法?尽管有这种可能性,我还是想出了一个解决办法
我的开始大纲是这样的:
+------------+----------------------------------------------+
| Field Name | Mapping Function |
+------------+----------------------------------------------+
| Name | Customer.Name -> Data.Customers.Name |
| Address | Customer.Address -> Data.Customers.Address |
| Age | Customer.Age -> Data.Customers.Age |
| Comments | Customer.Comments -> Data.Customers.Comments |
+------------+----------------------------------------------+
System.data.SqlClient.SqlBulkCopy
对象将数据插入目标表T[]
)转换成SqlBulkCopy
可以接受的内容。但要做到这一点,我需要决定如何映射字段
以下是我决定的:
var mapper = new List<Tuple<string, Func<object, string, object>>>();
事实上,它是一个列表,表示行。这就给我们留下了
元组
。这将创建一种字典(但索引),其中给定的字符串(字段名)映射到一个函数,当给定源对象T
和源字段(例如Address
)时,该函数将检索适当的值。如果没有为表字段找到相应的属性,我们将只返回null
验证输入值是否有效(连接有效、表存在等)后,我们将创建映射对象:
//get all the column names for the table to build mapping object
SqlCommand command = new SqlCommand($"SELECT TOP 1 * FROM {foundTableName}", conn);
SqlDataReader reader = command.ExecuteReader();
//build mapping object by iterating through rows and verifying that there is a match in the table
var mapper = new List<Tuple<string, Func<object, string, object>>>();
foreach (DataRow col in reader.GetSchemaTable().Rows)
{
//get column information
string columnName = col.Field<string>("ColumnName");
PropertyInfo property = typeof(T).GetProperty(columnName);
Func<object, string, object> map;
if (property == null)
{
//check if it's nullable and exit if not
bool nullable = col.Field<bool>("Is_Nullable");
if (!nullable)
return $"No corresponding property found for Non-nullable field '{columnName}'.";
//if it's nullable, create mapping function
map = new Func<object, string, object>((a, b) => null);
}
else
map = new Func<object, string, object>((src, fld) => typeof(T).GetProperty(fld).GetValue(src));
//add mapping object
mapper.Add(new Tuple<string, Func<object, string, object>>(columnName, map));
}
然后通过写入数据来完成:
//set up the bulk copy connection
SqlBulkCopy sbc = new SqlBulkCopy(conn, SqlBulkCopyOptions.TableLock |
SqlBulkCopyOptions.UseInternalTransaction, null);
sbc.DestinationTableName = foundTableName;
sbc.BatchSize = BATCH_SIZE;
sbc.WriteToServer(rows);
就这样!工作起来很有魅力,而且运行速度太快,我懒得进行基准测试(EF花了几分钟运行这些导入)
我应该注意,我遗漏了很多错误检查代码,可能还会添加更多。但是,如果您想完整地了解我的课程,请看以下内容:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
namespace DatabaseUtilities
{
public static class RapidDataTools
{
const int SCHEMA_SCHEMA_NAME = 1;
const int SCHEMA_TABLE_NAME = 2;
const int BATCH_SIZE = 1000;
/// <summary>
/// Imports an array of data into a specified table. It does so by mapping object properties
/// to table columns. Only properties with the same name as the column name will be copied;
/// other columns will be left null. Non-nullable columns with no corresponding property will
/// throw an error.
/// </summary>
/// <param name="connectionString"></param>
/// <param name="destTableName">Qualified table name (e.g. Admin.Table)</param>
/// <param name="sourceData"></param>
/// <returns></returns>
public static string Import<T>(string connectionString, string destTableName, T[] sourceData)
{
//get destination table qualified name
string[] tableParts = destTableName.Split('.');
if (tableParts.Count() != 2) return $"Invalid or unqualified destination table name: {destTableName}.";
string destSchema = tableParts[0];
string destTable = tableParts[1];
//create the database connection
SqlConnection conn = GetConnection(connectionString);
if (conn == null) return "Invalid connection string.";
//establish connection
try { conn.Open(); }
catch { return "Could not connect to database using provided connection string."; }
//make sure the requested table exists
string foundTableName = string.Empty;
foreach (DataRow row in conn.GetSchema("Tables").Rows)
if (row[SCHEMA_SCHEMA_NAME].ToString().Equals(destSchema, StringComparison.CurrentCultureIgnoreCase) &&
row[SCHEMA_TABLE_NAME].ToString().Equals(destTable, StringComparison.CurrentCultureIgnoreCase))
{
foundTableName = $"{row[SCHEMA_SCHEMA_NAME]}.{row[SCHEMA_TABLE_NAME]}";
break;
}
if (foundTableName == string.Empty) return $"Specified table '{destTableName}' could not be found in table.";
//get all the column names for the table to build mapping object
SqlCommand command = new SqlCommand($"SELECT TOP 1 * FROM {foundTableName}", conn);
SqlDataReader reader = command.ExecuteReader();
//build mapping object by iterating through rows and verifying that there is a match in the table
var mapper = new List<Tuple<string, Func<object, string, object>>>();
foreach (DataRow col in reader.GetSchemaTable().Rows)
{
//get column information
string columnName = col.Field<string>("ColumnName");
PropertyInfo property = typeof(T).GetProperty(columnName);
Func<object, string, object> map;
if (property == null)
{
//check if it's nullable and exit if not
bool nullable = col.Field<bool>("Is_Nullable");
if (!nullable)
return $"No corresponding property found for Non-nullable field '{columnName}'.";
//if it's nullable, create mapping function
map = new Func<object, string, object>((a, b) => null);
}
else
map = new Func<object, string, object>((src, fld) => typeof(T).GetProperty(fld).GetValue(src));
//add mapping object
mapper.Add(new Tuple<string, Func<object, string, object>>(columnName, map));
}
//get all the data
int dataCount = sourceData.Count();
var rows = new DataRow[dataCount];
DataTable destTableDT = new DataTable();
destTableDT.Load(reader);
for (int x = 0; x < dataCount; x++)
{
var dataRow = destTableDT.NewRow();
dataRow.ItemArray = mapper.Select(m => m.Item2.Invoke(sourceData[x], m.Item1)).ToArray();
rows[x] = dataRow;
}
//close the old connection
conn.Close();
//set up the bulk copy connection
SqlBulkCopy sbc = new SqlBulkCopy(conn, SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.UseInternalTransaction, null);
sbc.DestinationTableName = foundTableName;
sbc.BatchSize = BATCH_SIZE;
//establish connection
try { conn.Open(); }
catch { return "Failed to re-established connection to the database after reading data."; }
//write data
try { sbc.WriteToServer(rows); }
catch (Exception ex) { return $"Batch write failed. Details: {ex.Message} - {ex.StackTrace}"; }
//if we got here, everything worked!
return string.Empty;
}
private static SqlConnection GetConnection(string connectionString)
{
DbConnectionStringBuilder csb = new DbConnectionStringBuilder();
try { csb.ConnectionString = connectionString; }
catch { return null; }
return new SqlConnection(csb.ConnectionString);
}
}
}
使用系统;
使用System.Collections.Generic;
使用系统数据;
使用System.Data.Common;
使用System.Data.SqlClient;
使用System.Linq;
运用系统反思;
命名空间数据库实用程序
{
公共静态类RapidDataTools
{
const int SCHEMA_SCHEMA_NAME=1;
const int SCHEMA_TABLE_NAME=2;
常数int批量大小=1000;
///
///将数据数组导入到指定的表中。它通过映射对象属性来实现
///复制到表列。仅复制与列名同名的属性;
///其他列将保留为null。没有相应属性的不可为null的列将保留为null
///抛出一个错误。
///
///
///限定表名称(例如管理表)
///
///
公共静态字符串导入(字符串连接字符串、字符串destTableName、T[]源数据)
{
//获取目标表限定名
字符串[]tableParts=destTableName.Split('.');
if(tableParts.Count()!=2)返回$“无效或不合格的目标表名:{destTableName}。”;
字符串destSchema=tableParts[0];
字符串destable=tableParts[1];
//创建数据库连接
SqlConnection conn=GetConnection(connectionString);
if(conn==null)返回“无效连接字符串”;
//建立联系
试试{conn.Open();}
catch{return“无法使用提供的连接字符串连接到数据库。”;}
//确保请求的表存在
string foundTableName=string.Empty;
foreach(conn.GetSchema(“表”).Rows中的数据行)
if(行[SCHEMA\u SCHEMA\u NAME].ToString().Equals(destSchema,StringComparison.CurrentCultureIgnoreCase)&&
行[SCHEMA_TABLE_NAME].ToString().Equals(destTable,StringComparison.CurrentCultureIgnoreCase))
{
foundTableName=$“{row[SCHEMA\u SCHEMA\u NAME]}.{row[SCHEMA\u TABLE\u NAME]}”;
打破
}
if(foundTableName==string.Empty)返回$“在表中找不到指定的表“{destTableName}”;
//获取要生成映射对象的表的所有列名
SqlCommand=newSQLCommand($“从{foundTableName}中选择顶部1*”,conn);
SqlDataReader=command.ExecuteReader();
//通过遍历行并验证表中是否存在匹配项来构建映射对象
var mapper=新列表();
foreach(reader.GetSchemaTable().Rows中的DataRow列)
{
//获取列信息
字符串columnName=列字段(“columnName”);
PropertyInfo property=typeof(T).GetProperty(columnName);
Func图;
if(属性==null)
{
//检查它是否为空,如果不为空则退出
bool nullable=列字段(“Is_nullable”);
如果(!nullable)
return$“没有对应的
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
namespace DatabaseUtilities
{
public static class RapidDataTools
{
const int SCHEMA_SCHEMA_NAME = 1;
const int SCHEMA_TABLE_NAME = 2;
const int BATCH_SIZE = 1000;
/// <summary>
/// Imports an array of data into a specified table. It does so by mapping object properties
/// to table columns. Only properties with the same name as the column name will be copied;
/// other columns will be left null. Non-nullable columns with no corresponding property will
/// throw an error.
/// </summary>
/// <param name="connectionString"></param>
/// <param name="destTableName">Qualified table name (e.g. Admin.Table)</param>
/// <param name="sourceData"></param>
/// <returns></returns>
public static string Import<T>(string connectionString, string destTableName, T[] sourceData)
{
//get destination table qualified name
string[] tableParts = destTableName.Split('.');
if (tableParts.Count() != 2) return $"Invalid or unqualified destination table name: {destTableName}.";
string destSchema = tableParts[0];
string destTable = tableParts[1];
//create the database connection
SqlConnection conn = GetConnection(connectionString);
if (conn == null) return "Invalid connection string.";
//establish connection
try { conn.Open(); }
catch { return "Could not connect to database using provided connection string."; }
//make sure the requested table exists
string foundTableName = string.Empty;
foreach (DataRow row in conn.GetSchema("Tables").Rows)
if (row[SCHEMA_SCHEMA_NAME].ToString().Equals(destSchema, StringComparison.CurrentCultureIgnoreCase) &&
row[SCHEMA_TABLE_NAME].ToString().Equals(destTable, StringComparison.CurrentCultureIgnoreCase))
{
foundTableName = $"{row[SCHEMA_SCHEMA_NAME]}.{row[SCHEMA_TABLE_NAME]}";
break;
}
if (foundTableName == string.Empty) return $"Specified table '{destTableName}' could not be found in table.";
//get all the column names for the table to build mapping object
SqlCommand command = new SqlCommand($"SELECT TOP 1 * FROM {foundTableName}", conn);
SqlDataReader reader = command.ExecuteReader();
//build mapping object by iterating through rows and verifying that there is a match in the table
var mapper = new List<Tuple<string, Func<object, string, object>>>();
foreach (DataRow col in reader.GetSchemaTable().Rows)
{
//get column information
string columnName = col.Field<string>("ColumnName");
PropertyInfo property = typeof(T).GetProperty(columnName);
Func<object, string, object> map;
if (property == null)
{
//check if it's nullable and exit if not
bool nullable = col.Field<bool>("Is_Nullable");
if (!nullable)
return $"No corresponding property found for Non-nullable field '{columnName}'.";
//if it's nullable, create mapping function
map = new Func<object, string, object>((a, b) => null);
}
else
map = new Func<object, string, object>((src, fld) => typeof(T).GetProperty(fld).GetValue(src));
//add mapping object
mapper.Add(new Tuple<string, Func<object, string, object>>(columnName, map));
}
//get all the data
int dataCount = sourceData.Count();
var rows = new DataRow[dataCount];
DataTable destTableDT = new DataTable();
destTableDT.Load(reader);
for (int x = 0; x < dataCount; x++)
{
var dataRow = destTableDT.NewRow();
dataRow.ItemArray = mapper.Select(m => m.Item2.Invoke(sourceData[x], m.Item1)).ToArray();
rows[x] = dataRow;
}
//close the old connection
conn.Close();
//set up the bulk copy connection
SqlBulkCopy sbc = new SqlBulkCopy(conn, SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.UseInternalTransaction, null);
sbc.DestinationTableName = foundTableName;
sbc.BatchSize = BATCH_SIZE;
//establish connection
try { conn.Open(); }
catch { return "Failed to re-established connection to the database after reading data."; }
//write data
try { sbc.WriteToServer(rows); }
catch (Exception ex) { return $"Batch write failed. Details: {ex.Message} - {ex.StackTrace}"; }
//if we got here, everything worked!
return string.Empty;
}
private static SqlConnection GetConnection(string connectionString)
{
DbConnectionStringBuilder csb = new DbConnectionStringBuilder();
try { csb.ConnectionString = connectionString; }
catch { return null; }
return new SqlConnection(csb.ConnectionString);
}
}
}
using (var ctx = new EntitiesContext())
{
ctx.BulkInsert(list);
}