Задача: распаралелить медленную вставку записей в БД 1С, при чтении файла Excel
Как ни странно, но такая возможность есть и работает начиная с версии платформы 8.3.8. Т.е. можно запустить фоновое задание внутри фонового задания….ну например по вставке в БД большого количества записей, которая выполняется весьма медленно. Полноценно этот процесс распаралелить можно например как-то так:
Функция СделатьЗаписьвБДПДЗФоново(парам) экспорт
...
// делаем медленную запись в БД
...
конецфункции
// Просматриваем массив с идентификаторами фоновых заданий и удаляем те которые завершились
Функция ПочиститьМассивОтЗавершенныхЗаданий(МассивФоновыхЗаданий) экспорт
для каждого ФЗМ из МассивФоновыхЗаданий цикл
ФЗ = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(ФЗМ);
если ФЗ.Состояние<>СостояниеФоновогоЗадания.Активно тогда
МассивФоновыхЗаданий.Удалить(МассивФоновыхЗаданий.Найти(ФЗМ));
конецесли;
конеццикла;
возврат МассивФоновыхЗаданий;
конецфункции
// выполняем загрузку построчно файла Excel, запуская запись данных в БД
// отдельным потоком. Количество потоков ограничивам 10 штуками
Функция ВыполнитьЗагрузку(парам) экспорт
МассивФоновыхЗаданий=Новый Массив();
МаксимумЗаданий=10;
...
Для нСтрокаТФ = 2 ПО КолВоСтрокФайла Цикл
....
МассивПараметров = Новый Массив;
Параметры=Новый Структура("aa,bb,cc",
1,
2,
3,
);
МассивПараметров.Добавить(Параметры);
ФЗ = ФоновыеЗадания.Выполнить("ДлительныеОперации.СделатьЗаписьвБДПДЗФоново",МассивПараметров);
МассивФоновыхЗаданий.Добавить(ФЗ.УникальныйИдентификатор);
МассивФоновыхЗаданий=ПочиститьМассивОтЗавершенныхЗаданий(МассивФоновыхЗаданий);
// ждём пока завершаться запущеные потоки, если их больше 10
пока МассивФоновыхЗаданий.Количество()>МаксимумЗаданий цикл
МассивФоновыхЗаданий=ПочиститьМассивОтЗавершенныхЗаданий(МассивФоновыхЗаданий);
конеццикла;
....
конеццикла;
конецфункции
В ниже приведённом каркасе кода, фоново выполняется функция ВыполнитьЗагрузку(), которая читает большой файл Excel, затем каждый цикл вставки записи в БД запускает в свою очередь фоново, ограничивая в данном случае количество фоновых заданий 10 штуками.
Иногда удаётся найти подводные камни там, где совсем не ждешь от 1С. Оказывается в запросах 1С, сложение столбцов а+б даёт в сумме null, если одно из значений равно null. даже если чётко укажешь преобразовать значение в строку, например так:
ВЫБОР
КОГДА geo_coors_public_regions.name ЕСТЬ NULL
ТОГДА ""
ИНАЧЕ geo_coors_public_regions.name
КОНЕЦ + "," + ВЫБОР
КОГДА geo_coors_public_areas.name ЕСТЬ NULL
ТОГДА ""
ИНАЧЕ geo_coors_public_areas.name
КОНЕЦ + "," + ВЫБОР
КОГДА geo_coors_public_cities.name ЕСТЬ NULL
ТОГДА ""
ИНАЧЕ geo_coors_public_cities.name
КОНЕЦ + "," + ВЫБОР
КОГДА geo_coors_public_settlers.name ЕСТЬ NULL
ТОГДА ""
ИНАЧЕ geo_coors_public_settlers.name
КОНЕЦ
ИЗ
...
Задача: загрузить в табличный документ на форме файл большого размера, с индикацией прогресса загрузки с использованием фоновой работы.
Решение: если поддержка фоновой работы в 1С была уже довольно давно, то асинхронная загрузка файлов на сервер появилась лишь начиная с версии 8.3.15.1489 ,Ну тоже уже давно, но руки добрались начать использовать только сейчас, т.к. ранее было не критично — не настолько большие файлы загружал/обрабатывал.
Итак, сначала на форме разместим индикатор загрузки. Для этого в реквизитах формы необходимо создать переменную типа «число», и перетащив её на форму выбрать тип «индикатор»:
На кнопку «Загрузить ЛС» навесим открытие диалогового окна:
Фильтр = "Файл с лицевыми счетами(*.xlsx)|*.xlsx";
ПараметрыДиалога = новый ПараметрыДиалогаПомещенияФайлов("Выберите файлы XLSX", Истина, Фильтр);
А чуть ниже определим обработчики оповещения о ходе загрузки файла на сервер и окончании загрузки файла, которые укажем при вызове процедуры «НачатьПомещениеФайлаНаСервер» (есть еще и «НачатьПомещениеФайловНаСервер»). В итоге код получится такой:
В результате чего после выбора файла, по экрану побежит индикатор хода перемещения файла на сервер. Далее этот файл необходимо будет обработать на сервере фоново. И тут возникает один нюанс: мы не можем передать в фоновое задание ссылку на перемещенный файл во временном хранилище.Точнее можем, но фоновое задание это хранилище прочитать не может (это то ли глюк, то ли фича платформы — не понятно). Проблема.. Тогда делаем финт ушами: перед уходом в «фон», мы создадим временный файл во временной папке пользователя 1С, и передадим в фон уже не ссылку на него, а непосредственно имя временного файла. Для этого я просто написал функцию, которая на входе получает адрес загруженного файла, а на выходе даёт имя временного файла:
Кроме того, чтобы мы могли передать результат в обработку по завершению фоновой работы, нам необходимо при запуске фонового задания передать в него некоторые параметры, а именно:
имя созданного временного файла
колонки/строки откуда брать данные из эксель файла
адрес временного хранилища, куда поместить результат работы фонового задания
Функция которая будет работать фоново, должна размещаться в общем модуле. Это небольшой недостаток внешних обработок — фоново запускаются только процедуры-функции созданные внутри конфигурации. Но! до этого нам нужно опять же написать «обвязку» фонового задания, дабы мы имели возможность знать, работает или нет оно, а так-же на каком этапе. При запуске фонового задания, мы получаем идентификатор этого задания:
Функция ЗапуститьФЗЗагрузкиЛС(Параметры)
МассивПараметров = Новый Массив;
МассивПараметров.Добавить(Параметры);
ФЗ = ФоновыеЗадания.Выполнить("СК_ГР_ДлительныеОперации.ЗагрузитьФайлыЛС",МассивПараметров);
объект.ЛС_ФЗ = ФЗ.УникальныйИдентификатор;
КонецФункции
Который в дальнейшем будем использоваться для того чтобы «узнать» как поживает собственно это задание, подключив на клиенте обработчик ожидания, выполняющийся раз в секунду:
&НаКлиенте
Процедура ИндикаторВыполненияЗагрузкиФайловЛС() Экспорт
пр=КакДелаУФЗЗагрузкиФайловЛС();
если пр=неопределено тогда
этаформа.ИндикаторЗагрузкиЛС=100;
ОтключитьОбработчикОжидания("ИндикаторВыполненияЗагрузкиФайловЛС");
Этаформа.Элементы.ПояснениеКЗагрузкеЛС.Заголовок="Файлы обработаны";
иначе
этаформа.ИндикаторЗагрузкиЛС=пр;
конецесли;
КонецПроцедуры
&НаСервере
Функция КакДелаУФЗЗагрузкиФайловЛС()
ФЗ = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(объект.идФЗ);
если ФЗ=Неопределено тогда
возврат неопределено;
иначе
если ФЗ.Состояние=СостояниеФоновогоЗадания.Завершено тогда
возврат неопределено;
конецесли;
если ФЗ.Состояние=СостояниеФоновогоЗадания.ЗавершеноАварийно тогда
Сообщить("Ошибка:"+ФЗ.ИнформацияОбОшибке.Описание);
возврат неопределено;
конецесли;
конецесли;
возврат 0;
конецфункции
Как видим, тут отслеживается состояние фонового задания (запущен, работает, завершен, завершен с ошибкой), но не отслеживается этап выполнения работ. Есть на самом деле два способа получения хода работы фонового задания:
перехват вывода функции Сообщить() на сервере, и парсинг данных из него. Например в фоновом задании можно с какой-то периодичностью выводить что-то вроде: Сообщить(«12.3%загружаю»);, а получив на клиенте эту запись показывать индикацию 12.3% и соответствующее пояснение.
Можно во время работы фонового задания ложить данные во временно хранилище, и читать их из клиента. НО! данный способ работает только в случай файловой БД. Циатата из справки 1С: «Данные, помещенные во временное хранилище в фоновом задании, не будут доступны из родительского сеанса до момента завершения фонового задания»
Посему остаётся таки отлавливать сообщения сервера по известному идентификатору фонового задания:
&НаСервере
Функция ПолучитьСообщенияФЗ(ФЗ, Состояние = Неопределено, УдалятьСообщения = Ложь) Экспорт
Если Состояние = Неопределено Тогда
Состояние = ФЗ.Состояние;
КонецЕсли;
МассивСообщений = Новый Массив;
Сообщения = ФЗ.ПолучитьСообщенияПользователю(УдалятьСообщения);
Если Сообщения <> Неопределено Тогда
Для Каждого Сообщение Из Сообщения Цикл
МассивСообщений.Добавить(Сообщение.Текст);
КонецЦикла;
КонецЕсли;
Возврат МассивСообщений;
КонецФункции
А в самом фоновом здании, городить огород с выводом сообщений:
// На входе:
// парам.ВременныйФайл
// парам.Расширение
// парам.ЛС_начало
// парам.ЛС_лс
// парам.ЛС_то
// парам.ЛС_дата_установки_пу
// парам.АдресВременногХранилища
Функция ЗагрузитьФайлыЛСРасширенно(парам) экспорт
Сообщить("0%читаю файл на сервере");
ТабличныйДокументХар = Новый ТабличныйДокумент;
ТабличныйДокументХар.Прочитать(парам.ВременныйФайл,СпособЧтенияЗначенийТабличногоДокумента.Значение);
стар=0;
Для Каждого ОбластьТД ИЗ ТабличныйДокументХар.Области Цикл
ОбластьФайла = ТабличныйДокументХар.ПолучитьОбласть(ОбластьТД.Имя);
КолВоСтрокФайла = ОбластьФайла.ПолучитьРазмерОбластиДанныхПоВертикали();
КолВоКолонокФайла = ОбластьФайла.ПолучитьРазмерОбластиДанныхПоГоризонтали();
НачСтрока=парам.ЛС_начало;КонСтрока=0;
НачСтрока = ?(НачСтрока = 0, 2, НачСтрока);
КонСтрока = ?(КонСтрока = 0, КолвоСтрокФайла, КонСтрока);
//перебираем все строки без первой строки
Для нСтрокаТФ = НачСтрока ПО КонСтрока Цикл
если стар<>Окр(нСтрокаТФ*100/КонСтрока) тогда
Сообщить(Строка(Окр(нСтрокаТФ*100/КонСтрока))+"%обрабатываю файл");
стар=Окр(нСтрокаТФ*100/КонСтрока);
конецесли;
конеццикла;
конеццикла
конецфункции
Посему модифицируем и обработчик ожидания, добавив парсинг сообщений сервера:
&НаКлиенте
Процедура ИндикаторВыполненияЗагрузкиФайловЛС() Экспорт
пр=КакДелаУФЗЗагрузкиФайловЛС();
если пр=неопределено тогда
этаформа.ИндикаторЗагрузкиЛС=100;
ОтключитьОбработчикОжидания("ИндикаторВыполненияЗагрузкиФайловЛС");
Этаформа.Элементы.ПояснениеКЗагрузкеЛС.Заголовок="Файлы обработаны";
иначе
если пр.Количество()>0 тогда
этаформа.ИндикаторЗагрузкиЛС=пр[0];
Этаформа.Элементы.ПояснениеКЗагрузкеЛС.Заголовок=пр[1];
конецесли;
конецесли;
КонецПроцедуры
&НаСервере
Функция КакДелаУФЗЗагрузкиФайловЛС()
ФЗ = ФоновыеЗадания.НайтиПоУникальномуИдентификатору(объект.ЛС_ФЗ);
если ФЗ=Неопределено тогда
возврат неопределено;
иначе
если ФЗ.Состояние=СостояниеФоновогоЗадания.Завершено тогда
возврат неопределено;
конецесли;
если ФЗ.Состояние=СостояниеФоновогоЗадания.ЗавершеноАварийно тогда
Сообщить("Ошибка:"+ФЗ.ИнформацияОбОшибке.Описание);
возврат неопределено;
конецесли;
ФСообщения=СК_ГР_ДлительныеОперации.ПолучитьСообщенияФЗ(ФЗ,,истина);
Если ФСообщения.Количество() > 0 Тогда
Для Каждого Сообщение Из ФСообщения Цикл
Если СтрНайти(Сообщение,"%")>0 тогда
возврат СтрРазделить(Сообщение,"%");
конецесли;
КонецЦикла;
КонецЕсли;
конецесли;
возврат Новый Массив();
конецфункции
Намедни прилетела интересная задача: создать визуальный построитель адреса доставки счёта клиента согласно данным ФИАС в древней не обновляемой конфигурации 1С. Первоначально подумал покопать в сторону сервиса https://fias-public-service.nalog.ru/api/spas/v2.0/swagger/index.html, написал заявку на генерацию ключа API для доступа, отправил её на указанную электронную почту…и тишина. Тащить самому адреса ФИАС и файлов и создавать для этого обвязку в конфигурараторе — трудоёмко. Но неожиданно возникла идея — а есть же обновляемая регулярно конфигурация 1С «Бухгалтерия», где этот справочник вполне используется и регулярно обновляется. Следовательно мы можем просто создать..ну например WEB сервис, при помощи которого будем тянуть данные для построения адреса из Бухгалтерии. Можно данные тянуть из регистров АдресныйОбъект, ДополнительныеАдресныеСведения, ИерархияАдресов и т.д, а можно покопать БСП, найти все нужные функции. За сим и оформил сервис:
&НаСервере
Функция ПолучитьПрокси()
Определение = Новый WSОпределения(
Константы.СК_БУХURLВебСервиса.Получить(),
Константы.СК_БУХИмяПользователя.Получить(),
Константы.СК_БУХПароль.Получить()
//,ИнтернетПрокси
);
Прокси = Новый WSПрокси(
Определение,
Константы.СК_БУХURIПространстваИмен.Получить(),
"sk_SOAP_Services",
"sk_SOAP_ServicesSoap"
);
Прокси.Пользователь = Константы.СК_БУХИмяПользователя.Получить();
Прокси.Пароль = Константы.СК_БУХПароль.Получить();
Возврат Прокси;
КонецФункции
Функция sk_gr_fias_exchange(request, params)
params=JSONВСтруктуру(params);
МассивДляВозврата=Новый Массив();
if (request="GetAdresses") тогда
//Parent,Level,TypeAddress,Counts,Poz,FindStr
ДополнительныеПараметры=Новый Структура("КоличествоЗаписей,Позиция,СтрокаПоиска",params.Counts,params.Poz,params.FindStr);
ТЗ=АдресныйКлассификаторСлужебный.АдресаДляИнтерактивногоВыбора(Новый УникальныйИдентификатор(params.Parent), params.Level, params.TypeAddress, ДополнительныеПараметры);
МассивДляВозврата=Новый Массив();
для каждого стр из ТЗ.Данные цикл
инф=Новый Структура("Идентификатор,Представление",Строка(стр.Идентификатор),Строка(стр.Представление));
МассивДляВозврата.Добавить(инф);
конеццикла;
конецесли;
if (request="GetHomes") тогда
ТЗ=АдресныйКлассификаторСлужебный.СписокДомов(Новый УникальныйИдентификатор(params.Parent),params.FindStr,params.Counts);
МассивДляВозврата=Новый Массив();
для каждого стр из ТЗ цикл
инф=Новый Структура("Идентификатор,Представление,Индекс",Строка(стр.Идентификатор),Строка(стр.Представление),Строка(стр.Значение.Индекс));
МассивДляВозврата.Добавить(инф);
конеццикла;
конецесли;
Возврат СтруктураВСтрокуJSON(МассивДляВозврата);
КонецФункции
На целевой конфигурации, остаётся только дергать вызовы:
&НаСервере
Функция ПолучитьСписокАдресов(Родитель,Уровень)
// Уровень: 1- регион,2 - район, 3 - муниципальный район, 4-поселение, 5-город, 6 -нс, 7 - территория, 8 - улица
Прокси=ПолучитьПрокси();
params=Новый Структура("Parent,Level,TypeAddress,Counts,Poz,FindStr");
params.Parent=Строка(Родитель);
params.Level=Уровень;
params.TypeAddress="Административно-территориальный";
params.Counts=500;
params.FindStr="";
Результат=Прокси.sk_gr_fias_exchange("GetAdresses",СтруктураВСтрокуJSON(params));
//сообщить(Результат);
возврат JSONВСтруктуру(Результат);
КонецФункции
&НаСервере
Функция ПолучитьСписокДомов(Родитель)
Прокси=ПолучитьПрокси();
params=Новый Структура("Parent,Level,TypeAddress,Counts,Poz,FindStr");
params.Parent=Строка(Родитель);
params.Counts=500;
params.FindStr="";
Результат=Прокси.sk_gr_fias_exchange("GetHomes",СтруктураВСтрокуJSON(params));
//сообщить(Результат);
возврат JSONВСтруктуру(Результат);
КонецФункции