开发中经常遇到批量插入数据的需求,为了提高开发效率大多会使用ORM架构,个别之处 才会手写SQL,我们使用C#.NET Core5.0开发,所以优先选择了微软的EF。
但是EF原生没有批量操作功能,需要自己扩展或使用第三方的扩展,由于使用第三方扩展怕有风险,因此全部自己手写批量插入和更新。
一段时间后数据多了,这里发现EF查询性能较差,2百万条数据就开始慢得不想用喽,后来换成SqlSugar(下面简称SS),SS的查询确实与原生SQL差不多,很好,很强大,但是它自带的批量插入就不太好用喽,测试后发现性能与官方宣传的差距太大。
本文实测并记录了Oracle 11g r2 单张表113列,批量插入1万条数据的结果:
方法一:
描述 :使用参数数组插入(如果看不懂我的描述,可以继续向下看源代码);
耗时:插入1万条1.729秒
方法二 :
描述 :使用OracleBulkCopy插入;
耗时:插入1万条2.672秒
方法三 :
描述 :使用sugar的Fastest.BulkCopy插入;
耗时:插入1万条4.532秒
以上3个方法多次测试每次耗时略有差异,但基本一致 。
sugar的其他批量插入方法:
经测试只适合插入100条以内的数据,插入数据生成 INSERT ALL INTO Table() VALUES()语句,个人觉得大量数据不适用,因为数据插入过多后代码会耗尽数据库服务器的cpu,导致无响应的问题
db.Insertable(list).ExecuteCommand();
下面这种就更不行了,是一条一条插入的:
db.Insertable(targetList).UseParameter().ExecuteCommand();
方法一代码:
以下代码根据项目手工编写,经测试性能是最高的,就本文测试的环境下远远高于sqlsugar的性能 。
/// <summary>
/// 批量插入;要么全部成功要么全部失败
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list"></param>
/// <returns></returns>
public static int BulkAdd<T>(List<T> list)
{
Type model = typeof(T);
List<PropertyInfo> pi = GetMappedField<T>();
//生成数据源与参数
OracleParameter[] paras = new OracleParameter[pi.Count];//用于存放数据
for (int i = 0; i < pi.Count; i++)
{
PropertyInfo p = pi[i];
string name = p.Name;
Type pt = p.PropertyType;
//bool t1 = pt.IsGenericType;
//Console.WriteLine($"序号:{i + 1};字段{ p.Name};类型{ pt.Name };IsGenericType:{pt.IsGenericType}");
//if (name == "LLCLY")
//{
// string name2 = p.Name;
//}
//此字段是否可可为空
bool IsNullable = pt.Name == "Nullable`1";
var colArr = new object[list.Count];
for (var rowIndex = 0; rowIndex < list.Count; rowIndex++)
{
object val = p.GetValue(list[rowIndex], null);
//时间或整形不能为null
if (IsEnum(p))
{
//枚举单独处理
colArr[rowIndex] = val == null ? DBNull.Value : val.GetHashCode();
//Console.WriteLine($"序号:{i + 1};字段{ p.Name};类型{ pt.Name };值{val};hash:{val.GetHashCode()}");
continue;
}
else
{
if (IsNullable)
{
bool teste1 = pt.DeclaringType != null && pt.DeclaringType.IsEnum;
bool teste2 = p.DeclaringType != null && p.DeclaringType.IsEnum;
bool teste3 = pt.GetGenericArguments()[0].IsEnum;
if (IsEnum(p))
{
colArr[rowIndex] = (val == null || !IsNullable) ? DBNull.Value : val.GetHashCode();
}
//Console.WriteLine($"序号:{i + 1};字段{ p.Name};类型{ pt.Name }-{pt.GetGenericArguments()[0].Name};值{val}");
}
else
{
colArr[rowIndex] = (val == null && !IsNullable) ? 0 : val;
//Console.WriteLine($"序号:{i + 1};字段{ p.Name};类型{ pt.Name };值{val}");
}
}
}
//基本类型
OracleDbType dt = OracleDbType.Varchar2;
if (p.PropertyType.Namespace == "System")
{
if (IsNullable)
{
//可为空时找真实基本类型
pt = pt.GetGenericArguments()[0];
}
switch (pt.Name)
{
case "String":
dt = OracleDbType.Varchar2;
break;
case "Short":
case "Int":
case "Int16":
dt = OracleDbType.Int16;
break;
case "Int32":
dt = OracleDbType.Int32;
break;
case "Decimal":
dt = OracleDbType.Decimal;
break;
case "Long":
case "Int64":
case "Double":
dt = OracleDbType.Long;
break;
case "DateTime":
dt = OracleDbType.Date;
break;
default:
break;
}
}
else if (p.PropertyType.IsEnum || pt.BaseType.Name == "Enum" || p.PropertyType.BaseType.Name == "Enum")
{
//枚举单独处理
dt = OracleDbType.Int32;
}
paras[i] = new OracleParameter($":{name}", dt) { Value = colArr };
}
//获取表名
string tableName = model.Name;
TableAttribute[] arrDesc = (TableAttribute[])model.GetCustomAttributes(typeof(TableAttribute), false);
if (arrDesc.Length > 0)
{
//EF架构的表属性名
tableName = arrDesc.First().Name;
}
else
{
//sqlsugar架构的表属性名
SugarTable[] tableDesc = (SugarTable[])model.GetCustomAttributes(typeof(SugarTable), false);
tableName = tableDesc.First().TableName;
}
//获取字段
string[] propertys = pi.Select(o => o.Name).ToArray();
string[] paras_propertys = propertys.Select(o => $":{o}").ToArray();
int result = 0;
string sql = $"INSERT INTO {tableName}({string.Join(",", propertys)}) VALUES({string.Join(",", paras_propertys)})";
using (OracleConnection oracleConnection = new(Conn))
{
oracleConnection.Open();
using (var command = oracleConnection.CreateCommand())
{
command.ArrayBindCount = list.Count;
command.FetchSize = 1000;
command.CommandText = sql.ToString();
command.CommandType = CommandType.Text;
command.Parameters.AddRange(paras);
command.BindByName = true;
result = command.ExecuteNonQuery();
}
}
return result;
}
public static bool IsEnum(PropertyInfo p) {
Type pt = p.PropertyType;
return pt.IsEnum
|| pt.BaseType.Name == "Enum"
//标记可为空的枚举
|| (pt.GetGenericArguments().Length > 0 && pt.GenericTypeArguments.First().IsEnum);
//|| (pt.GetGenericArguments().Length > 0 && pt.GetGenericArguments()[0].IsEnum);
}
/// <summary>
/// 获取映射的字段
/// </summary>
/// <returns></returns>
public static List<PropertyInfo> GetMappedField<T>() {
Type model = typeof(T);
List<PropertyInfo> proTemp = model.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public).ToList();
//过滤出与表对应的字段
List<PropertyInfo> pi = new List<PropertyInfo>();
for (int i = 0; i < proTemp.Count; i++)
{
PropertyInfo p = proTemp[i];
string name = p.Name;
Type pt = p.PropertyType;
//是否泛型 如:List<User> Int? 等
//bool t1 = pt.IsGenericType;
//Console.WriteLine($"序号:{i + 1};字段{ p.Name};类型{ pt.Name };IsGenericType:{pt.IsGenericType}");
if (pt.BaseType == null)
{
continue;
}
//被标记不与数据库映射的字段SugarColumn
SugarColumn[] caArray = (SugarColumn[])p.GetCustomAttributes(typeof(SqlSugar.SugarColumn), true);
if (caArray.Any(a => a.IsIgnore == true))
{
continue;
}
Object[] caArray2 = p.GetCustomAttributes(typeof(NotMappedAttribute), true);
if (caArray2.Length > 0)
{
continue;
}
//基本类型
if (p.PropertyType.Namespace == "System")
{
pi.Add(p);
}
else if (p.PropertyType.IsEnum || pt.BaseType.Name == "Enum" || p.PropertyType.BaseType.Name == "Enum")
{
pi.Add(p);
}
else
{
//不与数据库映射的字段
continue;
}
}
return pi;
}
方法二代码:
/// <summary>
/// 批量插入数据
/// [有人说,会自动创建多余的列,还可能多一些数据,且删不掉,但是我没遇到过]
/// </summary>
/// <param name="table">数据表</param>
/// <param name="targetTableName">数据库目标表名</param>
/// <returns></returns>
public static bool ExcuteBulkData(DataTable table, string targetTableName=null)
{
bool result = false;
using (OracleConnection conn = new(Conn))
{
conn.Open();
using (OracleBulkCopy bulkCopy = new(Conn, OracleBulkCopyOptions.Default))
{
if (table != null && table.Rows.Count > 0)
{
bulkCopy.DestinationTableName = targetTableName?? table.TableName;
bulkCopy.BatchSize = table.Rows.Count;
for (int i = 0; i < table.Columns.Count; i++)
{
string col = table.Columns[i].ColumnName;
bulkCopy.ColumnMappings.Add(col, col);
}
bulkCopy.BulkCopyOptions = new() {
};
bulkCopy.WriteToServer(table);
result = true;
}
}
}
return result;
}
方法三源代码:
引用过sqlsugar后只需要下面一行代码即可实现批量插入,但是性能有限 。
sugar.db.Fastest<CardDel>().BulkCopy(targetList);
总结:
1、方法一适合所有场景 ;方法二可根据情况选择,性能略低于第一种;sqlsugar的批量插入根据情况选择,性能并没有官网宣传的那么高,以上经验全部来源于个人实践后总结,供大家参考 。
2、平时要多自己写实例测试才能得到真实的结果,不能只看官网的宣传。