У нас есть прецедент в нашем приложении, где пользователь запускает запрос, который приведет к вставке из 100-1000 строк.
После этой вставки нам нужен объект для продолжения обработки и создания большего количества объектов, которые являются внешними ключами к первоначально вставленным объектам, или, другими словами, нам нужен ИД основного ключа для вставленных объектов.
До сих пор мы использовали EF для этого в цикле foreach, это было слишком медленно и занимало около 15-20 секунд для завершения около 600 строк. (при блокировке пользователя, bad :()
Исходный код (также обрабатывает обновления, но мы не заботимся о производительности там, он не блокирует пользователей):
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 секунд, теперь занимает ~ 100 мс (!)
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, не изменяются (это нормально и ожидаемо), но мне нужны вставленные идентификаторы, чтобы продолжить работу над этими объектами.
Простым решением было бы сразу выбрать данные из базы данных, у меня есть данные под рукой, я могу просто переустановить его в другую коллекцию.
Но это решение кажется неверным, нет способа получить идентификаторы как часть вставки.
EDIT: простое решение работает, см. Принятый ответ ниже о том, как легко синхронизировать его с EF.
Может быть, я не должен использовать SqlBulkCopy (я ожидаю до 1000 строк, не более) и использовать что-то еще?
Обратите внимание: несколько связанных с этим вопросов и решений, похоже, отходят от EF ..
Ничто из того, что вы делаете через EF, никогда не будет таким быстрым, как SqlBulkCopy. Действительно, необработанные SQL INSERT
не так быстр. Поэтому вам просто нужно перечитать локации. Обновите запрос, перечитав его с помощью MergeOption.OverwriteChanges .
Если вы используете 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
Это запишет идентификатор для вставленных строк и вернет все новые строки. Пока ваш c # datatable соответствует формату dbo.YourType, вы можете передать это так же, как обычно вы передаете параметр SqlCommand.
SqlCommand.Parameters.Add("@YourType", YourDataTable)
Я понимаю, что это похоже на ваше предложение о повторном выборе данных, но выбор должен быть быстрым, поскольку он использует только столбец идентификатора. В то время как у вас по-прежнему возникает проблема с использованием вставки SQL, а не с массовой копией, вы возвращаетесь к более основанному на наборе решениям, а не к процедурному решению EF. Это очень похоже на принятый ответ в одной из ссылок, которые вы опубликовали, но я удалил пару этапов с использованием переменной таблицы.