Как вы уже определили, вы не можете сохранить из foreach
, который все еще рисует из базы данных с помощью активного считывателя.
Вызов ToList()
или ToArray()
подходит для небольших наборов данных, но когда у вас тысячи строк, вы будете использовать большой объем памяти.
Лучше загружать строки кусками.
public static class EntityFrameworkUtil
{
public static IEnumerable<T> QueryInChunksOf<T>(this IQueryable<T> queryable, int chunkSize)
{
return queryable.QueryChunksOfSize(chunkSize).SelectMany(chunk => chunk);
}
public static IEnumerable<T[]> QueryChunksOfSize<T>(this IQueryable<T> queryable, int chunkSize)
{
int chunkNumber = 0;
while (true)
{
var query = (chunkNumber == 0)
? queryable
: queryable.Skip(chunkNumber * chunkSize);
var chunk = query.Take(chunkSize).ToArray();
if (chunk.Length == 0)
yield break;
yield return chunk;
chunkNumber++;
}
}
}
Учитывая приведенные выше методы расширения, вы можете написать свой запрос следующим образом:
foreach (var client in clientList.OrderBy(c => c.Id).QueryInChunksOf(100))
{
// do stuff
context.SaveChanges();
}
Запрашиваемый объект, для которого вызывается этот метод, должен быть упорядочен. Это связано с тем, что Entity Framework поддерживает только IQueryable<T>.Skip(int)
для упорядоченных запросов, что имеет смысл, если учесть, что несколько запросов для разных диапазонов требуют упорядочения. быть стабильным. Если порядок не важен для вас, просто упорядочите по первичному ключу, поскольку он может иметь кластерный индекс.
Эта версия будет запрашивать базу данных партиями по 100. Обратите внимание, что SaveChanges()
вызывается для каждой сущности.
Если вы хотите значительно повысить пропускную способность, вам следует звонить SaveChanges()
реже. Вместо этого используйте такой код:
foreach (var chunk in clientList.OrderBy(c => c.Id).QueryChunksOfSize(100))
{
foreach (var client in chunk)
{
// do stuff
}
context.SaveChanges();
}
Это приводит к уменьшению количества обращений к базе данных в 100 раз. Конечно, каждый из этих звонков длится дольше, но в итоге вы все равно выходите далеко вперед. Ваш пробег может меняться, но это было для меня быстрее.
И это обходит исключение, которое вы видели.
РЕДАКТИРОВАТЬ Я вернулся к этому вопросу после запуска SQL Profiler и обновил несколько вещей для повышения производительности. Для тех, кто заинтересован, вот несколько примеров SQL, которые показывают, что создано БД.
Первый цикл не должен ничего пропускать, поэтому он проще.
SELECT TOP (100) -- the chunk size
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
FROM [dbo].[Clients] AS [Extent1]
ORDER BY [Extent1].[Id] ASC
Последующие вызовы должны пропускать предыдущие фрагменты результатов, поэтому вводится использование row_number
:
SELECT TOP (100) -- the chunk size
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
FROM (
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], row_number()
OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
FROM [dbo].[Clients] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 100 -- the number of rows to skip
ORDER BY [Extent1].[Id] ASC