EntityFramework中支持BulkInsert扩展
EntityFramework中支持BulkInsert扩展
本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。
前言
很显然,你应该不至于使用 EntityFramework 直接插入 10W 数据到数据库中,那可能得用上个几分钟。EntityFramework 最被人诟病的地方就是它的性能,处理大量数据时的效率。此种条件下,通常会转回使用 ADO.NET 来完成任务。
但是,如果已经在项目中使用了 EntityFramework,如果碰到需要直接向数据库中插入 10W 的数据的需求,引入 ADO.NET 和 SqlBulkCopy 的组合将打破 EntityFramework 作为 ORM 所带来的优势,我们不得不再次去编写那些 SQL 语句,关注表结构的细节,相应的代码可维护性也在下降。
那么,假设我们将 SqlBulkCopy 的功能封装为 EntityFramework 中的一个扩展方法,通过接口像外暴露 BulkInsert 方法。这样,我们既没有改变使用 EntityFramework 的习惯,同时也隐藏了 SqlBulkCopy 的代码细节,更重要的是,合理的封装演进出复用的可能性,可以在多个 Entity 表中使用。
环境准备
以下测试基于 EntityFramework 6.0.2 版本。
首先定义一个 Customer 类:

1 public class Customer
2 {
3 public long Id { get; set; }
4 public string Name { get; set; }
5 public string Address { get; set; }
6 public string Phone { get; set; }
7 }

通过 CustomerMap 类将 Entity 映射到数据库表结构:

1 public class CustomerMap : EntityTypeConfiguration<Customer>
2 {
3 public CustomerMap()
4 {
5 // Primary Key
6 this.HasKey(t => t.Id);
7
8 // Properties
9 this.Property(t => t.Name)
10 .IsRequired()
11 .HasMaxLength(256);
12
13 this.Property(t => t.Phone)
14 .IsRequired()
15 .HasMaxLength(256);
16
17 // Table & Column Mappings
18 this.ToTable("Customer", "STORE");
19 this.Property(t => t.Id).HasColumnName("Id");
20 this.Property(t => t.Name).HasColumnName("Name");
21 this.Property(t => t.Address).HasColumnName("Address");
22 this.Property(t => t.Phone).HasColumnName("Phone");
23 }
24 }

我们定义数据库的名字为 “Retail”,则使用 RetailEntities 类来实现 DbContext :

1 public class RetailEntities : DbContext
2 {
3 static RetailEntities()
4 {
5 Database.SetInitializer<RetailEntities>(
6 new DropCreateDatabaseAlways<RetailEntities>());
7 }
8
9 public RetailEntities()
10 : base("Name=RetailEntities")
11 {
12 }
13
14 public DbSet<Customer> Customers { get; set; }
15
16 protected override void OnModelCreating(DbModelBuilder modelBuilder)
17 {
18 modelBuilder.Configurations.Add(new CustomerMap());
19 }
20 }

将 DatabaseInitializer 设置为 DropCreateDatabaseAlways,这样我们可以保证每次都针对新表进行测试。
如果需要更复杂的模型,我们将基于如下的模型进行测试:

测试主机

数据库:Microsoft SQL Server 2012 (64-bit)
EntityFramework 插入 10W 数据需要多久
我们先来看下EntityFramework 插入 10W 数据需要多久。
构造 10W 个 Customer 实例:

1 int customerCount = 100000;
2
3 List<Customer> customers = new List<Customer>();
4 for (int i = 0; i < customerCount; i++)
5 {
6 Customer customer = new Customer()
7 {
8 Name = "Dennis Gao" + i,
9 Address = "Beijing" + i,
10 Phone = "18888888888" + i,
11 };
12 customers.Add(customer);
13
14 Console.Write(".");
15 }

使用如下语法来将上面构造的 10W 数据保存到数据库中:

1 using (RetailEntities context = new RetailEntities())
2 {
3 foreach (var entity in customers)
4 {
5 context.Customers.Add(entity);
6 }
7 context.SaveChanges();
8 }

通过 context.SaveChanges() 来保证一次事务提交。
为了计算使用时间,在上面代码的前后加上 Stopwatch 来计算:

1 Stopwatch watch = Stopwatch.StartNew();
2
3 using (RetailEntities context = new RetailEntities())
4 {
5 foreach (var entity in customers)
6 {
7 context.Customers.Add(entity);
8 }
9 context.SaveChanges();
10 }
11
12 watch.Stop();
13 Console.WriteLine(string.Format(
14 "{0} customers are created, cost {1} milliseconds.",
15 customerCount, watch.ElapsedMilliseconds));

然后运行,

好吧,我应该没有耐心等待它运行完。
现在减少数据量进行测试,将数据数量降低到 500 条,

空表插入500 条数据耗时 5652 毫秒。我多测了几遍,这个数据稳定在 5 秒以上。
将数据量改变到 1000 条,

将数据量改变到 1500 条,

将数据量改变到 10000 条,

那么我们估计下 10W 数据大概需要 10W / 500 * 2 = 至少 400 秒 = 至少 6 分钟。
好吧,慢是毋庸置疑的。
SqlBulkCopy 接口描述
Microsoft SQL Server 提供一个称为 bcp 的流行的命令提示符实用工具,用于将数据从一个表移动到另一个表(表既可以在同一个服务器上,也可以在不同服务器上)。 SqlBulkCopy 类允许编写提供类似功能的托管代码解决方案。 还有其他将数据加载到 SQL Server 表的方法(例如 INSERT 语句),但相比之下 SqlBulkCopy 提供明显的性能优势。
使用 SqlBulkCopy 类只能向 SQL Server 表写入数据。 但是,数据源不限于 SQL Server;可以使用任何数据源,只要数据可加载到 DataTable 实例或可使用 IDataReader 实例读取数据。
| WriteToServer(DataRow[]) | 将所提供的 DataRow 数组中的所有行复制到 SqlBulkCopy 对象的 DestinationTableName 属性指定的目标表中。 |
| WriteToServer(DataTable) | 将所提供的 DataTable 中的所有行复制到 SqlBulkCopy 对象的 DestinationTableName 属性指定的目标表中。 |
| WriteToServer(IDataReader) | 将所提供的 IDataReader 中的所有行复制到 SqlBulkCopy 对象的 DestinationTableName 属性指定的目标表中。 |
| WriteToServer(DataTable, DataRowState) | 只将与所提供 DataTable 中所提供行状态匹配的行复制到 SqlBulkCopy 对象的 DestinationTableName 属性指定的目标表中。 |
在 .NET 4.5 中还提供了支持 async 语法的接口。
这里,我们选用 DataTable 来构建数据源,将 10W 数据导入 DataTable 中。可以看出,我们需要构建出给定 Entity 类型所对应的数据表的 DataTable,将所有的 entities 数据插入到 DataTable 中。
构建 TableMapping 映射
此时,我并不想手工书写表中的各字段名称,同时,我可能甚至都不想关心 Entity 类到底被映射到了数据库中的哪一张表上。
此处,我们定义一个 TableMapping 类,用于存储一张数据库表的映射信息。
在获取和生成 TableMapping 之前,我们需要先定义和获取 DbMapping 类。

1 internal class DbMapping
2 {
3 public DbMapping(DbContext context)
4 {
5 _context = context;
6
7 var objectContext = ((IObjectContextAdapter)context).ObjectContext;
8 _metadataWorkspace = objectContext.MetadataWorkspace;
9
10 _codeFirstEntityContainer = _metadataWorkspace.GetEntityContainer("CodeFirstDatabase", DataSpace.SSpace);
11
12 MapDb();
13 }
14 }

通过读取 CodeFirstEntityContainer 中的元数据,我们可以获取到指定数据库中的所有表的信息。

1 private void MapDb()
2 {
3 ExtractTableColumnEdmMembers();
4
5 List<EntityType> tables =
6 _metadataWorkspace
7 .GetItems(DataSpace.OCSpace)
8 .Select(x => x.GetPrivateFieldValue("EdmItem") as EntityType)
9 .Where(x => x != null)
10 .ToList();
11
12 foreach (var table in tables)
13 {
14 MapTable(table);
15 }
16 }

进而,根据表映射类型的定义,可以获取到表中字段的映射信息。

1 private void MapTable(EntityType tableEdmType)
2 {
3 string identity = tableEdmType.FullName;
4 EdmType baseEdmType = tableEdmType;
5 EntitySet storageEntitySet = null;
6
7 while (!_codeFirstEntityContainer.TryGetEntitySetByName(baseEdmType.Name, false, out storageEntitySet))
8 {
9 if (baseEdmType.BaseType == null) break;
10 baseEdmType = baseEdmType.BaseType;
11 }
12 if (storageEntitySet == null) return;
13
14 var tableName = (string)storageEntitySet.MetadataProperties["Table"].Value;
15 var schemaName = (string)storageEntitySet.MetadataProperties["Schema"].Value;
16
17 var tableMapping = new TableMapping(identity, schemaName, tableName);
18 _tableMappings.Add(identity, tableMapping);
19 _primaryKeysMapping.Add(identity, storageEntitySet.ElementType.KeyMembers.Select(x => x.Name).ToList());
20
21 foreach (var prop in storageEntitySet.ElementType.Properties)
22 {
23 MapColumn(identity, _tableMappings[identity], prop);
24 }
25 }

然后,可以将表信息和字段信息存放到 TableMapping 和 ColumnMapping 当中。

internal class TableMapping
{
public string TableTypeFullName { get; private set; }
public string SchemaName { get; private set; }
public string TableName { get; private set; } public ColumnMapping[] Columns
{
get { return _columnMappings.Values.ToArray(); }
}
}

构建 DataTable 数据
终于,有了 TableMapping 映射之后,我们可以开始创建 DataTable 了。

1 private static DataTable BuildDataTable<T>(TableMapping tableMapping)
2 {
3 var entityType = typeof(T);
4 string tableName = string.Join(@".", tableMapping.SchemaName, tableMapping.TableName);
5
6 var dataTable = new DataTable(tableName);
7 var primaryKeys = new List<DataColumn>();
8
9 foreach (var columnMapping in tableMapping.Columns)
10 {
11 var propertyInfo = entityType.GetProperty(columnMapping.PropertyName, '.');
12 columnMapping.Type = propertyInfo.PropertyType;
13
14 var dataColumn = new DataColumn(columnMapping.ColumnName);
15
16 Type dataType;
17 if (propertyInfo.PropertyType.IsNullable(out dataType))
18 {
19 dataColumn.DataType = dataType;
20 dataColumn.AllowDBNull = true;
21 }
22 else
23 {
24 dataColumn.DataType = propertyInfo.PropertyType;
25 dataColumn.AllowDBNull = columnMapping.Nullable;
26 }
27
28 if (columnMapping.IsIdentity)
29 {
30 dataColumn.Unique = true;
31 if (propertyInfo.PropertyType == typeof(int)
32 || propertyInfo.PropertyType == typeof(long))
33 {
34 dataColumn.AutoIncrement = true;
35 }
36 else continue;
37 }
38 else
39 {
40 dataColumn.DefaultValue = columnMapping.DefaultValue;
41 }
42
43 if (propertyInfo.PropertyType == typeof(string))
44 {
45 dataColumn.MaxLength = columnMapping.MaxLength;
46 }
47
48 if (columnMapping.IsPk)
49 {
50 primaryKeys.Add(dataColumn);
51 }
52
53 dataTable.Columns.Add(dataColumn);
54 }
55
56 dataTable.PrimaryKey = primaryKeys.ToArray();
57
58 return dataTable;
59 }

通过 Schema 名称和表名称来构建指定 Entity 类型的 DataTable 对象。
然后将,entities 数据列表中的数据导入到 DataTable 对象之中。

1 private static DataTable CreateDataTable<T>(TableMapping tableMapping, IEnumerable<T> entities)
2 {
3 var dataTable = BuildDataTable<T>(tableMapping);
4
5 foreach (var entity in entities)
6 {
7 DataRow row = dataTable.NewRow();
8
9 foreach (var columnMapping in tableMapping.Columns)
10 {
11 var @value = entity.GetPropertyValue(columnMapping.PropertyName);
12
13 if (columnMapping.IsIdentity) continue;
14
15 if (@value == null)
16 {
17 row[columnMapping.ColumnName] = DBNull.Value;
18 }
19 else
20 {
21 row[columnMapping.ColumnName] = @value;
22 }
23 }
24
25 dataTable.Rows.Add(row);
26 }
27
28 return dataTable;
29 }

SqlBulkCopy 导入数据
终于,数据源准备好了。然后使用如下代码结构,调用 WriteToServer 方法,将数据写入数据库。

1 using (DataTable dataTable = CreateDataTable(tableMapping, entities))
2 {
3 using (SqlBulkCopy sqlBulkCopy = new SqlBulkCopy(transaction.Connection, options, transaction))
4 {
5 sqlBulkCopy.BatchSize = batchSize;
6 sqlBulkCopy.DestinationTableName = dataTable.TableName;
7 sqlBulkCopy.WriteToServer(dataTable);
8 }
9 }

看下保存 500 数据的效果,用时 1.9 秒。

看下保存 1W 数据的效果,用时 2.1 秒。

看下保存 10W 数据的效果,用时 7.5 秒。

再试下 100W 数据的效果,用时 27 秒。

封装 BulkInsert 扩展方法
我们可以为 DbContext 添加一个 BulkInsert 扩展方法。

1 internal static class DbContextBulkOperationExtensions
2 {
3 public const int DefaultBatchSize = 1000;
4
5 public static void BulkInsert<T>(this DbContext context, IEnumerable<T> entities, int batchSize = DefaultBatchSize)
6 {
7 var provider = new BulkOperationProvider(context);
8 provider.Insert(entities, batchSize);
9 }
10 }

IBulkableRepository 接口
在下面两篇文章中,我介绍了精炼的 IRepository 接口。

1 public interface IRepository<T>
2 where T : class
3 {
4 IQueryable<T> Query();
5 void Insert(T entity);
6 void Update(T entity);
7 void Delete(T entity);
8 }

当我们需要扩展 BulkInsert 功能时,可以通过继承来完成功能扩展。
1 public interface IBulkableRepository<T> : IRepository<T>
2 where T : class
3 {
4 void BulkInsert(IEnumberable<T> entities);
5 }
这样,我们就可以使用很自然的方式直接使用 BulkInsert 功能了。
代码在哪里
代码在这里:
参考资料
- SqlBulkCopy
- Table-Valued Parameters
- EntityFramework.BulkInsert
- Using SQL bulk copy with your LINQ-to-SQL datacontext
本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。
EntityFramework中支持BulkInsert扩展的更多相关文章
- EntityFramework 中支持 BulkInsert 扩展
本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载. 前言 很显然,你应该不至于使用 EntityFramework 直接插入 10W 数据到数据库中,那 ...
- EntityFramework中支持BulkInsert扩展(转载)
前言 很显然,你应该不至于使用 EntityFramework 直接插入 10W 数据到数据库中,那可能得用上个几分钟.EntityFramework 最被人诟病的地方就是它的性能,处理大量数据时的效 ...
- C# Webservice 解决在运行配置文件中指定的扩展时出现异常。 ---> System.Web.HttpException: 超过了最大请求长度问
摘自: http://blog.csdn.net/gulijiang2008/article/details/4482993 请在服务器端配置 方法一: 在通过WebService处理大数据量数据时出 ...
- 在 Windows Azure 网站中进行纵向扩展和横向扩展
编辑人员注释:本文章由 Windows Azure 网站团队的项目经理 Byron Tardif 撰写. 当您开始一个新的 Web 项目,或者刚刚开始开发一般的网站和应用程序时,您可能希望从小处着手. ...
- 浅析Thinkphp框架中运用phprpc扩展模式
浅析Thinkphp框架中应用phprpc扩展模式 这次的项目舍弃了原来使用Axis2做web服务端的 方案,改用phprpc实现,其一是服务端的thinkphp已集成有该模式接口,其二是phprpc ...
- MySQL中的空间扩展
目录 19.1. 前言 19.2. OpenGIS几何模型 19.2.1. Geometry类的层次 19.2.2. 类Geometry 19.2.3. 类Point 19.2.4. 类Curve 1 ...
- Bash中的数学扩展
Bash只支持整数运算,不支持浮点运算.如果需要进行浮点运算,需要使用bc程序.Bash中的数学扩展有两种形式:$[ expression ]或$(( expression )) 例子:$echo $ ...
- 让IIS6支持任意扩展名和未知扩展名的下载
IIS6的安全性提高了很多,为了防止扩展名欺骗带来的安全性问题,限制了扩展名MIME类型. IIS6 只为对具有已知文件扩展名的文件的请求提供服务.如果请求内容的文件扩展名未映射到已知的扩展,则服务器 ...
- SpreadJS 在 Angular2 中支持哪些事件?
SpreadJS 纯前端表格控件是基于 HTML5 的 JavaScript 电子表格和网格功能控件,提供了完备的公式引擎.排序.过滤.输入控件.数据可视化.Excel 导入/导出等功能,适用于 .N ...
随机推荐
- Android FM学习中的模块 FM启动过程
最近的研究FM模,FM是一家值我正在学习模块.什么可以从上层中可以看出. 上层是FM按钮的操作和界面显示,因此调用到FM来实现广播收听的功能. 看看Fm启动流程:例如以下图: 先进入FMRadio.j ...
- 关于JavaScript中的事件代理
今天面试某家公司Web前端开发岗位,前面的问题回答的都还算凑活,并且又问了一下昨天面试时做的一道数组去重问题的解题思路(关于数组去重问题,可以观赏我前几天写的:http://www.cnblogs.c ...
- react学习笔记1--基础知识
什么是react A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES[React是一个用于构建用户界面的JavaScript库.] React之所以快, ...
- STL algorithmi算法s_sorted和is_sorted_until(28)
is_sort原型: ::is_sorted default (1) template <class ForwardIterator> bool is_sorted (ForwardIte ...
- spring4.2完整web项目(使用html视图解析器)
完整配置springmvc4,最终视图选择的是html,非静态文件. 最近自己配置spring的时候,遇到很多问题,由于开发环境和版本的变化导致网友们给出的建议很多还是不能用的,可能还会有很多人会遇到 ...
- Android 从硬件到应用程序:一步一步爬上去 6 -- 我写的APP测试框架层硬件服务(终点)
创Android Applicationproject:采用Eclipse的Android插入ADT创Androidproject,project名字Gpio,创建完成后,project文件夹pack ...
- maven和libgdx
这一篇是关于maven和libgdx的.本来我准备用gradle(现已有gradle模板了),不过暂时有点小问题,而同时libgdx官方提供了maven支持,为了快速上手还是选用maven了. 博客已 ...
- Ionic项目中使用极光推送-android
对于Ionic项目中使用消息推送服务,Ionic官方提供了ngCordova项目,这个里面的提供了用angularjs封装好的消息推送服务(官方文档),使用的是GitHub上的 PushPlugin ...
- Codeforces 484B Maximum Value(高效+二分)
题目链接:Codeforces 484B Maximum Value 题目大意:给定一个序列,找到连个数ai和aj,ai%aj尽量大,而且ai≥aj 解题思路:类似于素数筛选法的方式,每次枚举aj,然 ...
- 对于GetBuffer() 与 ReleaseBuffer() 的一些分析
先 转载一段别人的文章 CString类的这几个函数, 一直在用, 但总感觉理解的不够透彻, 不时还实用错的现象. 今天抽时间和Nico一起分析了一下, 算是拨开了云雾: GetBuffer和Rele ...