При работе в файловом режиме «1С:Предприятия» многие операции выполняются быстро и незаметно для пользователя. Однако при переходе в клиент-серверный режим разработчики часто сталкиваются с резким падением производительности привычных алгоритмов. Типичный пример — чтение текстовых файлов (в том числе формата CSV) объемом в несколько мегабайт, которое на сервере начинает занимать часы вместо секунд. В этой статье мы разберем причины этой проблемы и пошагово реализуем максимально эффективную схему чтения данных, а также настроим удобную индикацию выполнения для пользователя.
Обратим внимание на исходный код, который часто становится источником проблем:
&НаКлиенте
Процедура РазобратьCSV()
Текст = Новый ЧтениеТекста(ФайлCSV, КодировкаТекста.ANSI);
Для СтрокаCSV = 1 По КоличествоСтрок Цикл
Строка = Текст.ПрочитатьСтроку();
// Внутри этого цикла происходит обработка и вызов серверных процедур
ЗаписатьСтрокуНаСервере(Строка);
КонецЦикла;
КонецПроцедуры
Основная ошибка здесь заключается в архитектурном «грехе» клиент-серверного взаимодействия — избыточных вызовах сервера в цикле (так называемый latency-эффект). Когда процедура выполняется на клиенте, а чтение или запись данных происходят в цикле по десяткам тысяч строк, программа вынуждена совершать сетевой запрос туда и обратно на каждую итерацию. Даже если задержка сети составляет всего 2-5 миллисекунд, при 30 000 строк суммарное время ожидания составит несколько минут или даже часов.
Для оптимизации этого процесса необходимо придерживаться фундаментального правила разработки в 1С: минимизировать количество переходов между клиентом и сервером. Файл должен быть передан на сервер целиком, и вся его обработка должна происходить в один сеанс работы серверного контекста.
Рассмотрим детально, как правильно организовать процесс загрузки файла в клиент-серверном режиме. Нам потребуется передать локальный файл с компьютера пользователя во ВременноеХранилище, а затем прочитать его на стороне сервера.
Разберем по шагам реализацию этого механизма:
Посмотрим на пример реализации кода:
&НаКлиенте
Процедура ЗагрузитьФайлИОбработать()
// Выбираем файл на компьютере пользователя
ДиалогВыбора = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
ДиалогВыбора.Фильтр = "Файлы данных (*.csv)|*.csv";
Если ДиалогВыбора.Выбрать() Тогда
ПутьКФайлу = ДиалогВыбора.ПолноеИмяФайла;
// Помещаем файл во временное хранилище
АдресХранилища = "";
ПоместитьФайл(АдресХранилища, ПутьКФайлу, , Ложь, УникальныйИдентификатор);
// Передаем управление на сервер ОДНИМ вызовом
ОбработатьФайлНаСервере(АдресХранилища);
ПоказатьПредупреждение(, "Файл успешно обработан!");
КонецЕсли;
КонецПроцедуры
Теперь напишем серверную часть, которая будет работать непосредственно на стороне сервера приложений «1С:Предприятие»:
&НаСервереБезКонтекста
Процедура ОбработатьФайлНаСервере(Знач АдресХранилища)
// Извлекаем двоичные данные файла из временного хранилища
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
// Получаем безопасный путь во временном каталоге сервера
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("csv");
ДвоичныеДанные.Записать(ИмяВременногоФайла);
// Быстрое чтение файла на сервере
ТекстФайла = Новый ЧтениеТекста(ИмяВременногоФайла, КодировкаТекста.ANSI);
ТаблицаДляЗаписи = ИнициализироватьТаблицуДанных();
ТекущаяСтрока = ТекстФайла.ПрочитатьСтроку();
Пока ТекущаяСтрока <> Неопределено Цикл
// Парсим строку и добавляем в коллекцию для массовой записи
РазложитьИДобавитьСтроку(ТекущаяСтрока, ТаблицаДляЗаписи);
ТекущаяСтрока = ТекстФайла.ПрочитатьСтроку();
КонецЦикла;
ТекстФайла.Закрыть();
// Массовая запись данных в БД в одной транзакции
ЗаписатьДанныеВБазу(ТаблицаДляЗаписи);
// Удаляем временный файл с диска сервера
Попытка
УдалитьФайлы(ИмяВременногоФайла);
Исключение
ЗаписьЖурналаРегистрации("ОчисткаВременныхФайлов", УровеньЖурналаРегистрации.Предупреждение, , ,
"Не удалось удалить временный файл: " + ИмяВременногоФайла);
КонецПопытки;
КонецПроцедуры
Такой подход сокращает время выполнения с нескольких часов до считанных секунд, так как все дисковые операции и транзакции к базе данных происходят внутри локальной сети сервера приложений и СУБД.
После переноса кода на сервер возникает новый вопрос: как показать пользователю, на каком этапе находится загрузка? Ведь пока выполняется серверный вызов, клиентский интерфейс «замораживается», ожидая ответа. Использование стандартных оповещений внутри серверного цикла снова приведет к замедлению работы из-за постоянного переключения контекстов.
Наиболее надежный и современный способ решения этой задачи в управляемом приложении — использование фоновых заданий в связке с временным хранилищем или стандартными механизмами Библиотеки Стандартных Подсистем (БСП).
Проанализируем алгоритм реализации индикации без использования БСП:
ВременноеХранилище.Рассмотрим программную реализацию этой схемы.
Код серверной процедуры, которая будет запущена в фоновом задании:
// Процедура общего модуля (должна быть доступна для фоновых заданий)
Процедура ВыполнитьИмпортФайлаВФоне(АдресХранилища, АдресПрогресса) Экспорт
ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
ИмяВременногоФайла = ПолучитьИмяВременногоФайла("csv");
ДвоичныеДанные.Записать(ИмяВременногоФайла);
// Предварительно подсчитаем строки для расчета процента
ВсегоСтрок = 0;
ТекстПодсчет = Новый ЧтениеТекста(ИмяВременногоФайла);
Пока ТекстПодсчет.ПрочитатьСтроку() <> Неопределено Цикл
ВсегоСтрок = ВсегоСтрок + 1;
КонецЦикла;
ТекстПодсчет.Закрыть();
// Основное чтение и обработка
Текст = Новый ЧтениеТекста(ИмяВременногоФайла, КодировкаТекста.ANSI);
ТекущаяСтрока = Текст.ПрочитатьСтроку();
НомерСтроки = 0;
Пока ТекущаяСтрока <> Неопределено Цикл
НомерСтроки = НомерСтроки + 1;
ОбработатьСтрокуДанных(ТекущаяСтрока);
// Каждые 100 строк обновляем прогресс
Если НомерСтроки % 100 = 0 Или НомерСтроки = ВсегоСтрок Тогда
Процент = Окр((НомерСтроки / ВсегоСтрок) * 100);
СостояниеПроцесса = Новый Структура;
СостояниеПроцесса.Вставить("Процент", Процент);
СостояниеПроцесса.Вставить("Статус", "Обработано строк: " + НомерСтроки + " из " + ВсегоСтрок);
ПоместитьВоВременноеХранилище(СостояниеПроцесса, АдресПрогресса);
КонецЕсли;
ТекущаяСтрока = Текст.ПрочитатьСтроку();
КонецЦикла;
Текст.Закрыть();
УдалитьФайлы(ИмяВременногоФайла);
КонецПроцедуры
Код модуля управляемой формы для запуска и отслеживания процесса:
&НаКлиенте
Перем ИдентификаторЗадания;
&НаКлиенте
Перем АдресПрогресса;
&НаКлиенте
Процедура ЗапуститьИмпортСПрогрессом()
// ... выбор файла пользователем и помещение в АдресХранилища ...
// Создаем уникальный адрес для хранения прогресса
АдресПрогресса = ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор);
// Запускаем фоновое задание на сервере
ПараметрыЗапуска = Новый Массив;
ПараметрыЗапуска.Добавить(АдресХранилища);
ПараметрыЗапуска.Добавить(АдресПрогресса);
ИдентификаторЗадания = ЗапуститьФоновоеЗаданиеНаСервере(ПараметрыЗапуска);
// Подключаем обработчик ожидания для обновления формы
ПодключитьОбработчикОжидания("ПроверитьСтатусИмпорта", 1); // проверка каждую секунду
КонецПроцедуры
&НаСервереБезКонтекста
Функция ЗапуститьФоновоеЗаданиеНаСервере(ПараметрыЗапуска)
ФоновоеЗадание = ФоновыеЗадания.Выполнить("ИмяОбщегоМодуля.ВыполнитьИмпортФайлаВФоне", ПараметрыЗапуска);
Возврат ФоновоеЗадание.УникальныйИдентификатор;
КонецФункции
&НаКлиенте
Процедура ПроверитьСтатусИмпорта()
// Получаем текущее состояние из хранилища
ДанныеПрогресса = ПолучитьДанныеПрогресса(АдресПрогресса);
Если ДанныеПрогресса <> Неопределено Тогда
// Обновляем реквизиты формы, связанные с графическим индикатором
Элементы.ИндикаторПрогресса.Значение = ДанныеПрогресса.Процент;
Элементы.НадписьСтатуса.Заголовок = ДанныеПрогресса.Статус;
КонецЕсли;
// Проверяем, завершилось ли фоновое задание
Если ЗаданиеЗавершено(ИдентификаторЗадания) Тогда
ОтключитьОбработчикОжидания("ПроверитьСтатусИмпорта");
ПоказатьПредупреждение(, "Загрузка завершена успешно!");
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Функция ПолучитьДанныеПрогресса(Знач Адрес)
Возврат ПолучитьИзВременногоХранилища(Адрес);
EndФункции
&НаСервереБезКонтекста
Функция ЗаданиеЗавершено(Знач Идентификатор)
Задание = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(Идентификатор);
Возврат (Задание <> Неопределено И Задание.Состояние <> СостояниеФоновогоЗадания.Активно);
КонецФункции
Если ваша конфигурация разработана на базе типовых решений от фирмы 1С, вам не нужно писать код отслеживания фоновых заданий вручную. В Библиотеке Стандартных Подсистем реализован мощный механизм длительных операций.
Достаточно запустить операцию через процедуру ДлительныеОперации.ЗапуститьВыполнениеВФоне(), передав ей имя экспортной процедуры. Система БСП сама откроет стандартную форму ожидания с анимированным индикатором прогресса и кнопкой отмены процесса, избавив вас от необходимости проектирования интерфейса вручную.