EntityFramework Core 5.0 VS SQLBulkCopy
EF Core 5.0伴随着.NET 5.0发布已有一段时日,本节我们来预估当大批量新增数据时,大概是多少区间我们应该考虑SQLBulkCopy而不是EF Core
SQLBulkCopy早出现于.NET Framework 2.0,将数据批量写入利用此类毫无疑问最佳,虽其来源任意,但此类仅适用于SQL Server,每个关系数据库都有其批量处理驱动,这里我们仅仅只讨论SQL Server
性能差异预估批量数据大小
首先给出我们需要用到的测试模型
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EFCOREDB { public class Test { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public DateTime CreateDateTime { get; set; } } }
DynamicModelCacheKeyFactory
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; namespace EFCOREDB { /// <summary> /// 分库分表使用 /// </summary> public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory { public object Create(DbContext context) => context is DynamicContext dynamicContext ? (context.GetType(), dynamicContext.CreateDateTime) : (object)context.GetType(); } }
EFCoreVSSqlBulkCopyContext
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EFCOREDB { public class EFCoreVSSqlBulkCopyContext : DbContext { public DateTime CreateDateTime { get; set; }//为了区分不同的表 public DbSet<Test> Tests { get; set; } //sqlserver连接字符串 Server=(localdb)\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True; //sqlserver连接字符串 server=127.0.0.1;database=DynamicContext;user=zy;password=zy; //oracle连接字符串 Data Source=127.0.0.1:1521/orcl;User Id=zy;Password=zy; //"DbConnectString": "Data Source=127.0.0.1:1521/orcl;User Id=zy;Password=zy;", //"DefaultSchema": "ZY", //"DbVersion": "11", //mysql连接字符串 server=127.0.0.1;database=DynamicContext;user=zy;password=zy; //public static string DbConnectString = "(localdb)\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True;"; //如果是oracle的话,Oracle连接字符串中并不包含数据名称,其实DefaultSchema就是数据库名称,音系需要下面的两个DefaultSchema,DbVersion字段 public static string DefaultSchema = "ZY";// public static string DbVersion = "11"; DbType dbType = DbType.SqlServer; #region OnConfiguring protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { switch (dbType) { case DbType.SqlServer: string DbConnectStringSqlServer = "(localdb)\mssqllocaldb;Database=DynamicContext;Trusted_Connection=True;"; DbConnectStringSqlServer = "server=127.0.0.1;database=DynamicContext;user=zy;password=zy;"; DbConnectStringSqlServer = "server=127.0.0.1;database=EFCoreVSSqlBulkCopyContext;user=sa;password=sa123;"; optionsBuilder.UseSqlServer(DbConnectStringSqlServer) .ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); //SQL SERVER 2012/ 2014 分页,用 OFFSET,FETCH NEXT改写ROW_NUMBER的用法 //从 SQL SERVER 2000 那个大家还在写TOP的年代,到2005的ROW_NUMBER,再到2012的OFFSET FETCH //optionsBuilder.UseSqlServer(DbConnectStringSqlServer,b=>b.UseRowNumberForPaging()) // .ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); break; case DbType.MySql: string DbConnectStringMySql = "server=127.0.0.1;database=DynamicContext;user=zy;password=zy;"; DbConnectStringMySql = "server=127.0.0.1;database=DynamicContext;user=root;password=123456;"; optionsBuilder.UseMySql(DbConnectStringMySql) .ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); break; case DbType.Oracle: string DbConnectStringOracle = "Data Source=127.0.0.1:1521/orcl;User Id=zy;Password=zy;"; optionsBuilder.UseOracle(DbConnectStringOracle, t => t.UseOracleSQLCompatibility(DbVersion)) .ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); break; default: throw new Exception("未查询到对应的数据库。。。"); } } //=> optionsBuilder.UseMySql(DbConnectString).ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); //=> optionsBuilder.UseOracle(DbConnectString).ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); //=> optionsBuilder.UseSqlServer(DbConnectString).ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) //=> optionsBuilder.UseInMemoryDatabase("DynamicContext") //.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>(); #endregion #region OnModelCreating protected override void OnModelCreating(ModelBuilder modelBuilder) { if (Database.IsOracle()) { modelBuilder.HasDefaultSchema(DefaultSchema); } modelBuilder.Entity<Test>(b => { b.ToTable(CreateDateTime.ToString("yyyyMMdd")); b.HasKey(p => p.Id); //b.Property(p => p.Id).HasColumnType("int").ValueGeneratedOnAdd(); //b.Property(p => p.Id).HasColumnType("int"); b.Property(p => p.Title).HasMaxLength(20); b.Property(p => p.Content).HasMaxLength(500); b.Property(p => p.CreateDateTime); }); } } #endregion }
接下来我们则需要模拟数据,为伪造实际生产数据,这里我们介绍一个包Bogus,此包专用来伪造数据,一直在更新从未间断,版本也达到32,如下:
此包中有针对用户类的模拟,具体使用这里就不详细展开,我们构造一个方法来伪造指定数量的用户数据,如下:
using Bogus; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace EFCOREDB { /// <summary> /// 产生模拟数据 /// </summary> public class GenerateMockData { /// <summary> /// 产生指定数据量的数据 /// </summary> /// <param name="count"></param> /// <returns></returns> public static IEnumerable<Test> GetTests(int count) { var profileGenerator = new Faker<Test>() .RuleFor(p => p.Title, v => v.Person.FirstName) .RuleFor(p => p.Content, v => v.Person.LastName) .RuleFor(p => p.CreateDateTime, v => v.Person.DateOfBirth); return profileGenerator.Generate(count); } } }
有了批量伪造数据,接下来我们再利用上下文去新增数据,然后分别打印伪造数据和新增成功所耗费时间,如下:
/// <summary> /// EntityFramework Core 5.0 上下文去新增数据,然后分别打印伪造数据和新增成功所耗费时间 /// </summary> public static async void TestGenerateInsertAndInsertWithEFCoreData() { Console.WriteLine("产生模拟数据"); var stopwatch = new Stopwatch(); stopwatch.Start(); var data = GenerateMockData.GetTests(10000); //产生10条模拟数据 //产生模拟数据, 花费时间:0.2591446 秒 //产生模拟数据, 花费时间:0.2591446 秒,插入数据的数据数量: 10,插入数据花费时间: 0.2411458 //产生10000条模拟数据 //产生模拟数据,花费时间:0.9874856 秒 //产生模拟数据,花费时间:0.9874856 秒,插入数据的数据数量: 10000,插入数据花费时间: 1.4542355 var TotalSeconds = stopwatch.Elapsed.TotalSeconds; Console.WriteLine($"产生模拟数据,花费时间:{TotalSeconds} 秒"); //上下文去新增数据,然后分别打印伪造数据和新增成功所耗费时间 var datetime1 = DateTime.Now; using var EFCoreVSSqlBulkCopyContext = new EFCoreVSSqlBulkCopyContext() { CreateDateTime = datetime1 }; EFCoreVSSqlBulkCopyContext.Database.EnsureCreated(); stopwatch.Restart(); EFCoreVSSqlBulkCopyContext.Tests.AddRange(data); await EFCoreVSSqlBulkCopyContext.AddRangeAsync(data); var result = await EFCoreVSSqlBulkCopyContext.SaveChangesAsync(); Console.WriteLine($"产生模拟数据,花费时间:{TotalSeconds} 秒,插入数据的数据数量:{result},插入数据花费时间:{stopwatch.Elapsed.TotalSeconds}"); }
新增100条数据太小,这里我们直接从批量10000条数据开始测试,此时我们将看到所存储数据和实际数据完全一样
通过SQL Server Profiler工具监控得到如下一堆语句如下
通过运行多次,当然也和笔记本配置有关(i7,6核,内存8G),但还是可以预估批量新增1000条大概耗时为毫秒级,如下:
当然你也可所以使用十万条或者百万条数据,由于本电脑的配置,因此没有测试使用十万条或者百万条数据,有兴趣的可以再往上增长数据自己测试一下,
接下来我们来看看利用SqlBulkCopy看看性能如何
/// <summary> /// SQLBulkCopy 上下文去新增数据,然后分别打印伪造数据和新增成功所耗费时间 /// </summary> public static async void TestGenerateAndInsertWithSqlBulkCopyData() { Console.WriteLine("产生模拟数据"); var stopwatch = new Stopwatch(); stopwatch.Start(); var data = GenerateMockData.GetTests(10000); //产生10000条模拟数据 //产生模拟数据,花费时间:1.0126009 秒 //产生模拟数据, 花费时间:1.0126009 秒,插入数据的数据数量: 10000,插入数据花费时间: 0.3210853 var TotalSeconds = stopwatch.Elapsed.TotalSeconds; Console.WriteLine($"产生模拟数据,花费时间:{TotalSeconds} 秒"); //上下文去新增数据,然后分别打印伪造数据和新增成功所耗费时间 var datetime1 = DateTime.Now; using (var EFCoreVSSqlBulkCopyContext = new EFCoreVSSqlBulkCopyContext() { CreateDateTime = datetime1 }) { EFCoreVSSqlBulkCopyContext.Database.EnsureCreated(); } stopwatch.Restart(); var dt = new DataTable(); dt.Columns.Add("Id"); dt.Columns.Add("Title"); dt.Columns.Add("Content"); dt.Columns.Add("CreateDateTime"); foreach (var item in data) { dt.Rows.Add(item.Id, item.Title, item.Content, item.CreateDateTime); } //注意DestinationTableName 必须是全路径即 数据库名称.架构名称.表名称 using var sqlbulkcopy = new SqlBulkCopy("server=127.0.0.1;database=EFCoreVSSqlBulkCopyContext;user=sa;password=sa123;") { DestinationTableName = "EFCoreVSSqlBulkCopyContext.dbo.[20201208]" }; await sqlbulkcopy.WriteToServerAsync(dt); Console.WriteLine($"产生模拟数据,花费时间:{TotalSeconds} 秒,插入数据的数据数量:{10000},插入数据花费时间:{stopwatch.Elapsed.TotalSeconds}"); }
因如上利用EF Core新增时间在毫秒级,那么我们则直接从新增1万条开始测试,如下我们可看到此时与EF Core新增1万条数据差异,耗时远远小于1.6秒
还可以继续增长数据,本文不在展示测试结果,测试10万条数据很显然EF Core耗时结果将为SqlBulkCopy的指数倍(大致14倍,若数据为100万,想想二者其性能差异)
若继续通过SQL Server Profiler监控工具查看SQL语句,很显然SQL语句会很简短,据我所知,SqlBulkCopy是直接将数据通过流形式传输到数据库服务器,然后一次性插入到目标表中,所以性能是杠杠的。
利用SqlBulkCopy和EF Core 5.0,当然理论上不论是EF Core更新到其他任何版本,其性能与SqlBulkCopy不可同日而语,本文我们只是稍加探讨下数据量达到多少时不得不考虑其他手段来处理,而不是利用EF Core新增数据
EF Core和SqlBulkCopy性能差异比较,毫无疑问SqlBulkCopy为最佳选手,当新增数据量达到1万+时,若需考虑性能,可采用SqlBulkCopy或其他手段处理数据而不再是EF Core,二者性能差异将呈指数增长