При работе с большими объемами данных, хранящихся в BLOB полях SQL Server, часто возникает задача извлечения этих данных в файлы. В Delphi, при использовании компонентов доступа к данным, таких как FireDAC (FDQuery), можно столкнуться с ошибкой "Out of Memory", особенно при обработке десятков тысяч файлов. В этой статье мы рассмотрим причины возникновения этой ошибки и предложим несколько решений, ориентированных на Delphi и Object Pascal.
Проблема:
Программа пытается извлечь большое количество файлов из BLOB колонки в SQL Server. Файлы успешно сохраняются в директорию, но после обработки около 30,000 файлов возникает ошибка "Out of Memory". Предполагается, что ресурсы освобождаются после обработки каждого файла, и используются опции выборки данных для больших наборов.
Причины возникновения ошибки "Out of Memory":
Чрезмерная агрегация данных в памяти: Использование FDQuery1.FetchOptions.Mode := fmAll; приводит к тому, что FireDAC пытается загрузить все данные из запроса в память сразу. Для больших наборов данных это быстро исчерпывает доступную память.
Неэффективное управление памятью: Даже при попытках освобождения ресурсов, утечки памяти в коде или неоптимальное использование памяти могут приводить к постепенному увеличению потребления памяти и, как следствие, к ошибке.
32-битная архитектура приложения: 32-битные приложения имеют ограничение на объем используемой памяти (обычно 2-4 ГБ). Если объем данных, обрабатываемых приложением, превышает этот лимит, возникает ошибка "Out of Memory".
Решение:
Основная проблема в предоставленном коде - использование FDQuery1.FetchOptions.Mode := fmAll;. Это заставляет FireDAC загружать все данные в память сразу. Вместо этого, следует использовать режим fmOnDemand и обеспечить последовательную обработку данных.
Предлагаемое решение (с исправлениями):
procedure TForm1.ExtractInvoicesFromBlobs(AMinInvNo, AMaxInvNo: Integer);
var
PrintDate, FileName, BaseFolder, TargetFolder: string;
FileStream: TFileStream;
BlobStream: TStream;
RecordCount: Integer;
begin
// init
RecordCount := 0;
Memo1.Clear;
// retrieve data from db
FDQuery1.SQL.Clear;
FDQuery1.SQL.Text := 'SELECT ID, PrintedFile, Filename, PrintDateTime ' +
'FROM InvoiceStorage ' +
'WHERE Filename BETWEEN :MinFilename AND :MaxFilename';
FDQuery1.ParamByName('MinFilename').AsString := Format('%d.pdf', [AMinInvNo]);
FDQuery1.ParamByName('MaxFilename').AsString := Format('%d.pdf', [AMaxInvNo]);
// Важно: Установите Unidirectional и удалите fmAll перед Open
FDQuery1.FetchOptions.Unidirectional := True;
//FDQuery1.FetchOptions.Mode := fmAll; // УДАЛИТЬ ЭТУ СТРОКУ!
FDQuery1.Open;
while not FDQuery1.Eof do
begin
try
PrintDate := Copy(FDQuery1.FieldByName('PrintDateTime').AsString, 1, 4);
BaseFolder := IncludeTrailingPathDelimiter(Edit3.Text);
TargetFolder := Format(BaseFolder + '%s', [PrintDate]);
ForceDirectories(TargetFolder);
BlobStream := FDQuery1.CreateBlobStream(FDQuery1.FieldByName('PrintedFile'), bmRead);
try
FileName := IncludeTrailingPathDelimiter(TargetFolder) + FDQuery1.FieldByName('Filename').AsString;
FileStream := TFileStream.Create(FileName, fmCreate);
try
FileStream.CopyFrom(BlobStream, BlobStream.Size);
finally
FileStream.Free;
end;
finally
BlobStream.Free;
BlobStream := nil; // Важно обнулять ссылки после освобождения памяти
end;
// Each 1,000 files: log and provide some breathing air to the system
Inc(RecordCount);
if RecordCount mod 1000 = 0 then
begin
Memo1.Lines.Add(Format('%d files processed...', [RecordCount]));
TrimMemo;
Application.ProcessMessages;
Sleep(100);
end;
except
on E: Exception do
Memo1.Lines.Add(Format('Error while processing record %d: %s', [RecordCount, E.Message]));
end;
FDQuery1.Next;
end;
FDQuery1.Close;
end;
Альтернативные решения и дополнительные рекомендации:
Пакетная обработка: Разделите задачу на несколько этапов, обрабатывая, например, по 10,000 файлов за раз. После обработки каждой партии, закройте и снова откройте запрос. Это позволит освободить память между итерациями.
procedure TForm1.ExtractInvoicesFromBlobs(AMinInvNo, AMaxInvNo: Integer; ABatchSize: Integer);
var
CurrentMinInvNo, CurrentMaxInvNo: Integer;
begin
CurrentMinInvNo := AMinInvNo;
while CurrentMinInvNo <= AMaxInvNo do
begin
CurrentMaxInvNo := CurrentMinInvNo + ABatchSize - 1;
if CurrentMaxInvNo > AMaxInvNo then
CurrentMaxInvNo := AMaxInvNo;
ExtractInvoicesFromBlobsBatch(CurrentMinInvNo, CurrentMaxInvNo); // Отдельная процедура для обработки пакета
CurrentMinInvNo := CurrentMaxInvNo + 1;
end;
end;
procedure TForm1.ExtractInvoicesFromBlobsBatch(AMinInvNo, AMaxInvNo: Integer);
// ... (код из предыдущего примера, адаптированный для обработки пакета)
begin
// ...
try
FDQuery1.Open;
// ... (обработка данных)
finally
FDQuery1.Close;
end;
end;
Перекомпиляция в 64-битное приложение: Перекомпилируйте приложение под 64-битную архитектуру. Это позволит использовать больший объем памяти. В Delphi это можно сделать в настройках проекта.
Использование потоков (Threads): Разделите задачу извлечения файлов на несколько потоков. Каждый поток будет отвечать за обработку определенной части данных. Это позволит более эффективно использовать ресурсы процессора и памяти. (Более сложный вариант).
Профилирование памяти: Используйте инструменты профилирования памяти, чтобы выявить утечки памяти в коде. Delphi IDE предоставляет встроенные инструменты для профилирования.
Оптимизация SQL запроса: Убедитесь, что SQL запрос оптимизирован для быстрого извлечения данных. Индексы могут значительно улучшить производительность.
Проверка наличия достаточного места на диске: Убедитесь, что на диске, куда сохраняются файлы, достаточно места. Нехватка места на диске также может приводить к ошибкам.
Важные замечания:
Обнуление ссылок: После освобождения объектов (например, BlobStream.Free;) рекомендуется обнулять ссылки на эти объекты (BlobStream := nil;). Это помогает избежать случайного повторного использования освобожденной памяти.
Обработка исключений: Тщательно обрабатывайте исключения, чтобы предотвратить утечки ресурсов в случае ошибок. Используйте блоки try...finally для гарантированного освобождения ресурсов.
Application.ProcessMessages и Sleep: Использование Application.ProcessMessages позволяет приложению обрабатывать события Windows (например, перерисовку экрана), что делает его более отзывчивым. Sleep дает системе небольшую передышку. Однако, злоупотребление этими функциями может снизить производительность.
Заключение:
Ошибка "Out of Memory" при извлечении больших объемов данных из BLOB полей в Delphi может быть вызвана различными факторами. Использование правильных опций выборки данных, эффективное управление памятью, перекомпиляция в 64-битную архитектуру и пакетная обработка данных - это основные методы, позволяющие избежать этой ошибки и успешно обработать большие объемы данных. Не забывайте про профилирование памяти и оптимизацию SQL запросов для достижения максимальной производительности.
Контекст описывает причины возникновения ошибки "Out of Memory" при извлечении больших объемов данных из BLOB полей SQL Server в Delphi и предлагает решения, такие как использование режима `fmOnDemand`, пакетная обработка, перекомпиляция в 64-битную архи
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.