基于Entity Framework实现Sql Server批量增改(类似Oracle Merge)需求咨询
刚好之前做过定期拉取WebService数据批量同步到SQL Server的需求,太懂你说的AddRange+SaveChanges生成一堆SQL、慢到头疼的问题了!下面给你分享几个靠谱的方案,从原生高性能到省心的第三方库都有,按需选就行:
1. 临时表 + SqlBulkCopy + 原生Merge语句(首推,性能拉满)
这是处理大量数据时最稳妥的方式,完全对标Oracle的Merge逻辑,而且是数据库层面的批量操作,不会像EF那样逐条生成SQL。步骤很清晰:
第一步:创建临时表
先在数据库里建一个和目标表结构完全匹配的临时表(唯一键要和目标表对应),比如目标表是[dbo].[YourTable],临时表就用#TempYourTable。第二步:用SqlBulkCopy批量导数据
把从WebService拿到的数据集,通过SqlBulkCopy快速写入临时表——这一步的速度比EF的AddRange快N倍,毕竟是专门为批量数据设计的API。第三步:执行Merge语句
写一条原生SQL的Merge语句,对比唯一键,不存在就插入,存在就更新。
给你贴个可直接参考的代码示例:
using (var context = new YourDbContext()) { var dataFromWebService = GetDataFromWebService(); // 替换成你的数据获取方法 // 1. 创建临时表(如果已存在就先删掉) context.Database.ExecuteSqlRaw(@" IF OBJECT_ID('tempdb..#TempYourTable') IS NOT NULL DROP TABLE #TempYourTable; CREATE TABLE #TempYourTable ( Id INT PRIMARY KEY, -- 这里是你的唯一键字段 Column1 NVARCHAR(50), Column2 DATETIME, -- 其他字段和目标表保持一致 );"); // 2. 用SqlBulkCopy批量导入数据到临时表 using (var connection = new SqlConnection(context.Database.GetConnectionString())) { connection.Open(); using (var bulkCopy = new SqlBulkCopy(connection)) { bulkCopy.DestinationTableName = "#TempYourTable"; // 显式映射列名(如果实体字段和临时表列名完全一致,也可以省略这步) bulkCopy.ColumnMappings.Add("Id", "Id"); bulkCopy.ColumnMappings.Add("Column1", "Column1"); bulkCopy.ColumnMappings.Add("Column2", "Column2"); // 把实体列表转成DataTable(网上有很多现成的转换工具类,或者自己写个简单的) var dataTable = ConvertEntitiesToDataTable(dataFromWebService); bulkCopy.WriteToServer(dataTable); } } // 3. 执行Merge语句,完成批量Upsert context.Database.ExecuteSqlRaw(@" MERGE INTO [dbo].[YourTable] AS Target USING #TempYourTable AS Source ON Target.Id = Source.Id -- 唯一键匹配条件 WHEN MATCHED THEN UPDATE SET Target.Column1 = Source.Column1, Target.Column2 = Source.Column2 -- 其他需要更新的字段都列在这里 WHEN NOT MATCHED THEN INSERT (Id, Column1, Column2) VALUES (Source.Id, Source.Column1, Source.Column2);"); }
2. EF Core第三方扩展库:EFCore.BulkExtensions(省心首选)
如果不想写原生SQL和临时表的代码,直接用EFCore.BulkExtensions这个NuGet包就行——它底层也是用SqlBulkCopy+Merge实现的,性能和方案一差不多,但代码简洁到离谱,一行BulkMergeAsync就能搞定。
示例代码:
using (var context = new YourDbContext()) { var dataFromWebService = GetDataFromWebService(); await context.BulkMergeAsync(dataFromWebService, options => { options.MatchOn = new List<string> { "Id" }; // 指定用来判断重复的唯一键 // 还可以配置批量大小、忽略某些不需要更新的字段等 }); }
只需要先通过NuGet安装这个包,剩下的交给它就行,特别适合快速开发的场景。
3. EF原生批量处理(适合小数据量)
如果你的数据量不大(比如几百条以内),不想折腾第三方库也不想写原生SQL,用EF原生的方法也能凑活。思路是先查出已存在的记录ID,然后分插入和更新两组处理:
using (var context = new YourDbContext()) { var dataFromWebService = GetDataFromWebService(); // 先查数据库里已存在的ID var existingIds = context.YourTable .Where(t => dataFromWebService.Select(d => d.Id).Contains(t.Id)) .Select(t => t.Id) .ToList(); // 插入不存在的记录 var toInsert = dataFromWebService.Where(d => !existingIds.Contains(d.Id)).ToList(); context.YourTable.AddRange(toInsert); // 批量更新已存在的记录(用EF Core 3.0+的ExecuteUpdate,比逐条改状态高效) foreach (var item in dataFromWebService.Where(d => existingIds.Contains(d.Id))) { context.YourTable .Where(t => t.Id == item.Id) .ExecuteUpdate(setters => setters.SetProperty(t => t.Column1, item.Column1) .SetProperty(t => t.Column2, item.Column2)); } await context.SaveChangesAsync(); }
不过这个方案的缺点很明显:数据量大的时候,查询existingIds会很慢,而且更新还是会生成多条SQL,性能远不如前两种,只适合小数据量场景。
最后给你个选型参考:
- 数据量上千/上万条:优先选方案一(灵活可控)或方案二(省心省力),两者性能几乎没差;
- 数据量几百条以内:用方案三就行,贴合EF原生写法,不用额外依赖。
内容的提问来源于stack exchange,提问作者user10465350

