我们的应用程序中有一个用例,用户触发的请求将导致插入100到1000行 在插入之后,我们需要对象继续处理并创建更多对象,这些对象是最初插入的对象的外键,换句话说,我们需要插入对象的主键ID。
到目前为止,我们已经使用EF在foreach循环中执行此操作,这太慢了,并且需要大约15-20秒来完成大约600行。 (在阻止用户时,坏:()
原始代码(也处理更新,但我们不关心那里的性能,它不会阻止用户):
foreach (Location updatedLoc in locationsLoaded)
{
// find it in the collection from the database
Location fromDb = existingLocations.SingleOrDefault(loc => loc.ExtId.Equals(updatedLoc.ExtId));
// update or insert
if (fromDb != null)
{
// link ids for update
updatedLoc.Id = fromDb.Id;
// set values for update
db.Entry(fromDb).CurrentValues.SetValues(updatedLoc);
}
else
{
System.Diagnostics.Trace.WriteLine("Adding new location: " + updatedLoc.Name, "loadSimple");
// insert a new location <============ This is the bottleneck, takes about 20-40ms per row
db.Locations.Add(updatedLoc);
}
}
// This actually takes about 3 seconds for 600 rows, was actually acceptable
db.SaveChanges();
所以在研究了SO和互联网之后,我发现我使用EF的方式不对,需要使用SqlBulkCopy
因此代码被重写了,过去需要大约20秒,现在需要~100ms(!)
foreach (Location updatedLoc in locationsLoaded)
{
// find it in the collection from the database
Location fromDb = existingLocations.SingleOrDefault(loc => loc.ExtId.Equals(updatedLoc.ExtId));
// update or insert
if (fromDb != null)
{
// link ids for update
updatedLoc.Id = fromDb.Id;
// set values for update
db.Entry(fromDb).CurrentValues.SetValues(updatedLoc);
}
else
{
System.Diagnostics.Trace.WriteLine("Adding new location: " + updatedLoc.Name, "loadSimple");
// insert a new location
dataTable.Rows.Add(new object[] { \\the 14 fields of the location.. });
}
}
System.Diagnostics.Trace.WriteLine("preparing to bulk insert", "loadSimple");
// perform the bulk insert
using (var bulkCopy = new System.Data.SqlClient.SqlBulkCopy(System.Configuration.ConfigurationManager.ConnectionStrings["bulk-inserter"].ConnectionString))
{
bulkCopy.DestinationTableName = "Locations";
for (int i = 0; i < dataTable.Columns.Count; i++)
{
bulkCopy.ColumnMappings.Add(i, i + 1);
}
bulkCopy.WriteToServer(dataTable);
}
// for update
db.SaveChanges();
问题是,在批量复制之后,Locations
集合中的对象(EF ORM的一部分)不会更改(这是好的和预期的),但我需要插入的ID才能继续处理这些对象。
一个简单的解决方案是立即从数据库中再次选择数据,我手边有数据,我可以简单地将其重新选择到不同的集合中。
但该解决方案感觉不正确,是否无法将ID作为插入的一部分。
编辑:简单的解决方案有效,请参阅下面接受的答案,了解如何轻松将其同步回EF。
也许我不应该使用SqlBulkCopy(我希望最多约1000行,不再使用)并使用其他东西?
请注意,一些相关的SO问题和解决方案,似乎都离开了EF ..
答案 0 :(得分:4)
你通过EF做的事情都不会像SqlBulkCopy一样快。实际上,原始SQL INSERT
并不快。所以你只需要重新阅读地点。通过使用MergeOption.OverwriteChanges重新阅读来刷新查询。
答案 1 :(得分:4)
如果您使用的是SQL-Server 2008或更高版本,则可以使用存储过程来执行您所需的操作。您需要定义一个TYPE
,它与SQL中的数据表相同:
CREATE TYPE dbo.YourType AS TABLE (ID INT, Column1 INT, Column2 VARCHAR(5)...)
然后将此类型传递给存储过程。
CREATE PROCEDURE dbo.InsertYourType (@YourType dbo.YourType READONLY)
AS
BEGIN
DECLARE @ID TABLE (ID INT NOT NULL PRIMARY KEY)
INSERT INTO YourTable (Column1, Column2...)
OUTPUT inserted.ID INTO @ID
SELECT Column1, Column2...
FROM @YourType
SELECT *
FROM YourTable
WHERE ID IN (SELECT ID FROM @ID)
END
这将捕获插入行的ID,并返回所有新行。只要你的c#datatable符合dbo.YourType的格式,就可以像通常将参数传递给SqlCommand那样传递它。
SqlCommand.Parameters.Add("@YourType", YourDataTable)
我意识到这与您重新选择数据的建议类似,但是选择应该很快,因为它只使用标识列。虽然您仍然遇到使用SQL插入而不是批量复制的问题,但您将恢复到基于集合的解决方案,而不是基于过程的EF解决方案。这与您发布的其中一个链接中的已接受答案非常相似,但我已使用表变量删除了几个阶段。
答案 2 :(得分:0)
集:
yourContext.Configuration.AutoDetectChangesEnabled = false;
yourContext.Configuration.ValidateOnSaveEnabled = false;
在100个插入包中执行SaveChanges()...尝试使用1000并查看更改。
因为在所有这些插入期间,上下文是相同的,所以您可以每1000次插入重建上下文对象。 var yourContext = new YourContext();
在我的导入数据流程中进行此改进,从7分钟到6秒。
实际数字......在你的情况下不可能是100的1000 ...尝试并调整它。