При разработке обработок на платформе 1С:Предприятие 8.3 мы часто сталкиваемся с задачами, требующими длительной обработки больших объемов данных, например, из табличных частей формы. Если такую операцию выполнять синхронно, пользовательский интерфейс 1С будет заблокирован, что крайне негативно сказывается на удобстве работы. Возникает необходимость выполнять такие операции в фоновом режиме, сохраняя при этом возможность взаимодействия пользователя с формой. В этом материале мы подробно разберем, как реализовать подобный механизм, используя возможности платформы и стандартные подсистемы.
Прежде чем погрузиться в реализацию, давайте разберем, какие типы модулей существуют в 1С и как их можно использовать для фоновых заданий, поскольку в ходе обсуждения нередко возникает путаница.
Этот модуль содержит процедуры и функции, которые работают с конкретным экземпляром объекта. Он имеет прямой доступ к реквизитам объекта (его контексту), что очень удобно при разработке. Однако, при попытке запустить процедуры из этого модуля в фоновом режиме, мы сталкиваемся с ограничениями:
Таким образом, прямое использование модуля объекта для фоновых заданий, работающих с данными табличных частей, становится проблематичным без дополнительных манипуляций.
Модуль менеджера содержит статические методы, которые относятся ко всему типу объектов (например, ко всему справочнику "Номенклатура"), а не к конкретному экземпляру. Мы можем думать о нем как об общем модуле, который метаданными "привязан" к определенному типу объекта. Его особенности:
Справочники.Номенклатура.ПолучитьНаименование().Модуль менеджера подходит для операций, не требующих прямого контекста конкретного объекта или работающих со множеством объектов одного типа.
Общий модуль – это наиболее универсальный тип модуля. Он не привязан к конкретному объекту и не имеет собственного контекста. Мы можем вызывать экспортные процедуры и функции общего модуля из любого места конфигурации, включая фоновые задания.
Для выполнения длительных операций без блокировки пользовательского интерфейса в 1С используется механизм фоновых заданий. В типовых конфигурациях, основанных на Библиотеке стандартных подсистем (БСП), для управления фоновыми заданиями используются общие модули ДлительныеОперации и ДлительныеОперацииКлиент.
Эти модули предоставляют унифицированный подход для:
Важно: Процедура или функция, которую мы хотим выполнить в фоновом задании, должна быть экспортной и может находиться в общем модуле, модуле менеджера объекта или модуле объекта обработки. Однако, как мы уже выяснили, для работы с табличными частями напрямую из модуля объекта есть свои нюансы.
Основное препятствие при передаче данных в фоновые задания – требование сериализуемости всех параметров. Табличные части (ТЧ) объектов 1С, а также сам объект обработки/документа, не являются сериализуемыми напрямую.
Для обхода этого ограничения используется механизм временного хранилища с помощью функций ПоместитьВоВременноеХранилище() и ПолучитьИзВременногоХранилища().
Мы помещаем несериализуемые данные (например, ТаблицаЗначений, ДвоичныеДанные) во временное хранилище на сервере. В ответ получаем адрес хранилища – это простая строка, которая является сериализуемой. Этот адрес мы можем без проблем передать в фоновое задание.
Внутри фонового задания по переданному адресу мы можем извлечь данные из временного хранилища. Точно так же результаты выполнения фоновой операции могут быть помещены в новое временное хранилище, а его адрес возвращен на клиент.
Если нам нужно работать с данными табличной части в фоне, мы сначала выгружаем ее в ТаблицуЗначений (ТЗ). ТаблицаЗначений, в отличие от ТабличнойЧасти, является сериализуемой. Затем эту ТЗ помещаем во временное хранилище и передаем ее адрес. По завершении фоновой операции, полученную ТЗ из временного хранилища можно загрузить обратно в табличную часть формы.
Выгрузка и загрузка ТЧ в ТЗ и обратно, а также работа с временным хранилищем, требуют определенных временных и ресурсных затрат. Мы должны учитывать это при проектировании. Однако, для действительно длительных операций (несколько минут и более) это является необходимым компромиссом для обеспечения отзывчивости интерфейса. Для операций, занимающих секунды, выигрыш может быть незначительным или отрицательным из-за накладных расходов.
Давайте рассмотрим конкретный пример. У нас есть форма обработки с табличной частью, в которой пользователь вводит данные для создания новой номенклатуры. Мы хотим, чтобы процесс создания номенклатуры выполнялся в фоне.
Предположим, у нас есть следующая процедура в модуле объекта обработки, которая сейчас работает синхронно:
&НаСервере
Процедура СоздатьНовуюНоменклатуруСервер()
Отказ = Ложь;
Если НЕ ЗначениеЗаполнено(Объект.ВидНоменклатурыПоУмолчанию) Тогда
Сообщить("Настройки номенклатуры - Не указан Вид номенклатуры (по умолчанию)!");
Отказ = Истина;
КонецЕсли;
Если НЕ ЗначениеЗаполнено(Объект.ВидНоменклатурыПоУмолчанию.ЕдиницаИзмерения) Тогда
Сообщить("Настройки номенклатуры - Вид номенклатуры - не заполнена единица по умолчанию.!");
Отказ = Истина;
КонецЕсли;
Если Отказ Тогда
Сообщить("Перед началом работы - заполните обязательные настройки номенклатуры.");
Возврат;
КонецЕсли;
// Этот блок копирует данные, если процедура вызывается из формы
ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
ОбработкаОбъект.СоздатьНовуюНоменклатуруСервер(); // Предполагаем, что эта процедура работает с ТЧ объекта
ЗначениеВРеквизитФормы(ОбработкаОбъект, "Объект");
КонецПроцедуры
В данном примере ОбработкаОбъект.СоздатьНовуюНоменклатуруСервер() в модуле объекта напрямую работает с табличной частью Объект.ТЧ. Наша задача – перенести эту логику в фон.
Первым делом нам нужно вынести логику, работающую с табличной частью и настройками, в отдельную экспортную процедуру (или функцию) общего модуля, которая будет принимать все необходимые данные в качестве параметров. Почему именно общий модуль? Потому что он не имеет контекста и является наиболее гибким для вызова как из фонового задания, так и из синхронного кода или регламентного задания.
Мы создадим новый общий модуль (например, МояОбработкаБэкграунд) и в нем разместим нашу процедуру. Эта процедура больше не будет зависеть от Объект.ТЧ, а будет получать ТаблицуЗначений и настройки как параметры.
// ОбщийМодуль: МояОбработкаБэкграунд (Сервер)
// Должна быть экспортной процедурой
Процедура СоздатьНовуюНоменклатуруФоново(АдресВходныхДанных, АдресВыходныхДанных) Экспорт
// 1. Получаем входные данные из временного хранилища
Параметры = ПолучитьИзВременногоХранилища(АдресВходныхДанных);
ТЗНоменклатура = Параметры.ТаблицаНоменклатуры;
ВидНоменклатурыПоУмолчанию = Параметры.ВидНоменклатурыПоУмолчанию;
ЕдиницаИзмеренияПоУмолчанию = Параметры.ЕдиницаИзмеренияПоУмолчанию;
// ... также получаем другие необходимые настройки из Параметры
РезультатОбработки = Новый Структура;
РезультатОбработки.ВЫполненныеДействия = Новый Массив; // Можно хранить созданные ссылки, ошибки и т.д.
РезультатОбработки.Ошибки = Новый Массив;
// 2. Выполняем основную логику обработки данных
// Проходим по ТЗНоменклатура и создаем элементы справочника Номенклатура
Для Каждого СтрокаТЗ Из ТЗНоменклатура Цикл
Попытка
НоваяНоменклатура = Справочники.Номенклатура.СоздатьЭлемент();
НоваяНоменклатура.Наименование = СтрокаТЗ.Наименование;
НоваяНоменклатура.ВидНоменклатуры = ВидНоменклатурыПоУмолчанию;
НоваяНоменклатура.ЕдиницаИзмерения = ЕдиницаИзмеренияПоУмолчанию;
// ... заполняем другие реквизиты из СтрокаТЗ и настроек
НоваяНоменклатура.Записать();
// Сохраняем результат, например, ссылку на созданную номенклатуру
РезультатОбработки.ВЫполненныеДействия.Добавить(НоваяНоменклатура.Ссылка);
СтрокаТЗ.СсылкаНоменклатуры = НоваяНоменклатура.Ссылка; // Обновляем данные в ТЗ для возврата
СтрокаТЗ.Статус = "Создана";
Исключение
// Обрабатываем ошибки, сохраняем в результаты
РезультатОбработки.Ошибки.Добавить(Новый Структура("Строка,Описание", СтрокаТЗ, ОписаниеОшибки()));
СтрокаТЗ.Статус = "Ошибка";
КонецПопытки;
КонецЦикла;
// 3. Помещаем обновленную ТЗ и другие результаты во временное хранилище
РезультатОбработки.ОбновленнаяТаблицаНоменклатуры = ТЗНоменклатура; // Возвращаем обновленную ТЗ
ПоместитьВоВременноеХранилище(РезультатОбработки, АдресВыходныхДан);
КонецПроцедуры
Обратите внимание, что процедура СоздатьНовуюНоменклатуруФоново принимает два адреса временного хранилища: один для входных данных и один для выходных. Также внутри нее происходит работа с ТаблицейЗначений (ТЗНоменклатура), а не напрямую с табличной частью объекта.
Теперь мы изменим процедуру в модуле формы, которая вызывается по кнопке, чтобы она запускала фоновое задание.
// Модуль формы обработки
&НаКлиенте
Процедура СоздатьНовуюНоменклатуру()
// Проверка заполнения настроек (можно перенести на сервер или в фоновую)
Отказ = Ложь;
Если НЕ ЗначениеЗаполнено(Объект.ВидНоменклатурыПоУмолчанию) Тогда
Сообщить("Настройки номенклатуры - Не указан Вид номенклатуры (по умолчанию)!");
Отказ = Истина;
КонецЕсли;
Если НЕ ЗначениеЗаполнено(Объект.ВидНоменклатурыПоУмолчанию.ЕдиницаИзмерения) Тогда
Сообщить("Настройки номенклатуры - Вид номенклатуры - не заполнена единица по умолчанию.!");
Отказ = Истина;
КонецЕсли;
Если Отказ Тогда
Сообщить("Перед началом работы - заполните обязательные настройки номенклатуры.");
Возврат;
КонецЕсли;
// 1. Подготавливаем параметры для фонового задания
ПараметрыВхода = Новый Структура;
// Выгружаем табличную часть формы в ТаблицуЗначений
ПараметрыВхода.Вставить("ТаблицаНоменклатуры", Объект.ТЧ.Выгрузить());
// Передаем настройки
ПараметрыВхода.Вставить("ВидНоменклатурыПоУмолчанию", Объект.ВидНоменклатурыПоУмолчанию);
ПараметрыВхода.Вставить("ЕдиницаИзмеренияПоУмолчанию", Объект.ВидНоменклатурыПоУмолчанию.ЕдиницаИзмерения);
// ... Добавляем все необходимые для работы процедуры настройки из реквизитов формы/объекта
// 2. Помещаем входные данные во временное хранилище на сервере
АдресВходныхДанных = ПоместитьВоВременноеХранилищеНаСервере(ПараметрыВхода);
// 3. Создаем адрес для результата
АдресВыходныхДанных = ПоместитьВоВременноеХранилищеНаСервере(Неопределено); // Пустое хранилище для результата
// 4. Запускаем фоновое задание
ОповещениеОЗавершении = Новый ОписаниеОповещения("ОбработкаЗавершена", ЭтотОбъект, АдресВыходныхДанных);
ПараметрыЗадания = Новый Структура;
ПараметрыЗадания.Вставить("Метод", "ОбщийМодуль.МояОбработкаБэкграунд.СоздатьНовуюНоменклатуруФоново");
ПараметрыЗадания.Вставить("Параметры", Новый Массив);
ПараметрыЗадания.Параметры.Добавить(АдресВходныхДанных);
ПараметрыЗадания.Параметры.Добавить(АдресВыходныхДанных);
// Используем БСП для запуска длительной операции
ПараметрыДлительнойОперации = ДлительныеОперацииКлиент.ПараметрыВыполненияПроцедуры(ПараметрыЗадания);
ПараметрыДлительнойОперации.НаименованиеФоновогоЗадания = "Создание номенклатуры из обработки";
ДлительныеОперацииКлиент.ЗапуститьВыполнение(ОповещениеОЗавершении, ПараметрыДлительнойОперации);
Сообщить("Операция создания номенклатуры запущена в фоновом режиме. Пожалуйста, ожидайте завершения.");
КонецПроцедуры
// Вспомогательная функция для помещения во временное хранилище с сервера
&НаСервере
Функция ПоместитьВоВременноеХранилищеНаСервере(Данные)
Возврат ПоместитьВоВременноеХранилище(Данные, УникальныйИдентификатор);
КонецФункции
В этом коде мы видим несколько важных моментов:
ПараметрыВхода, куда помещаем все данные, необходимые для фонового задания: выгруженную табличную часть формы (Объект.ТЧ.Выгрузить()) и все необходимые настройки из реквизитов формы.ПоместитьВоВременноеХранилищеНаСервере(). Нам возвращается адрес хранилища.ДлительныеОперацииКлиент. Мы указываем полный путь к экспортной процедуре общего модуля ("ОбщийМодуль.МояОбработкаБэкграунд.СоздатьНовуюНоменклатуруФоново") и передаем ей адреса временных хранилищ как параметры.ОбработкаЗавершена.Когда фоновое задание завершится, метод, указанный в ОписаниеОповещения, будет вызван на клиенте. В этом методе мы должны получить результаты из временного хранилища и обновить форму.
// Модуль формы обработки
&НаКлиенте
Процедура ОбработкаЗавершена(РезультатВыполнения, ДополнительныеПараметры) Экспорт
Если РезультатВыполнения.Статус = СтатусВыполненияДлительнойОперации.Выполнено Тогда
// Фоновое задание успешно завершено
АдресВыходныхДанных = ДополнительныеПараметры; // Это переданный адрес выходного хранилища
// Получаем результаты из временного хранилища
Результаты = ПолучитьРезультатыИзВременногоХранилищаНаСервере(АдресВыходныхДанных);
Если Результаты <> Неопределено Тогда
// 1. Загружаем обновленную ТаблицуЗначений обратно в табличную часть формы
Объект.ТЧ.Загрузить(Результаты.ОбновленнаяТаблицаНоменклатуры);
Сообщить("Номенклатура успешно создана и таблица обновлена!", СтатусСообщения.Информация);
// 2. Обрабатываем ошибки, если они были
Если Результаты.Ошибки.Количество() > 0 Тогда
Сообщить("В процессе обработки возникли ошибки:", СтатусСообщения.Важное);
Для Каждого ОшибкаТек Из Результаты.Ошибки Цикл
Сообщить("Строка: " + ОшибкаТек.Строка.Наименование + ", Описание: " + ОшибкаТек.Описание, СтатусСообщения.Внимание);
КонецЦикла;
КонецЕсли;
// Можно также обновить другие реквизиты формы, если они менялись
Иначе
Сообщить("Не удалось получить результаты из временного хранилища.", СтатусСообщения.Ошибка);
КонецЕсли;
ИначеЕсли РезультатВыполнения.Статус = СтатусВыполненияДлительнойОперации.Ошибка Тогда
Сообщить("При выполнении операции создания номенклатуры произошла ошибка: " + РезультатВыполнения.КраткоеПредставлениеОшибки, СтатусСообщения.Ошибка);
Иначе
Сообщить("Выполнение операции отменено или неизвестный статус.", СтатусСообщения.Внимание);
КонецЕсли;
КонецПроцедуры
// Вспомогательная функция для получения из временного хранилища с сервера
&НаСервере
Функция ПолучитьРезультатыИзВременногоХранилищаНаСервере(Адрес)
Возврат ПолучитьИзВременногоХранилища(Адрес);
КонецФункции
В этой процедуре мы проверяем статус выполнения фонового задания. Если оно успешно завершено, мы по переданному адресу получаем структуру с результатами из временного хранилища. Затем мы загружаем обновленную ТаблицуЗначений обратно в табличную часть формы с помощью Объект.ТЧ.Загрузить() и выводим информацию о завершении и возможных ошибках.
Одним из преимуществ такого подхода является возможность использовать одну и ту же основную логику обработки (процедуру СоздатьНовуюНоменклатуруФоново в нашем ОбщемМодуле.МояОбработкаБэкграунд) как для интерактивного фонового выполнения, так и для регламентных заданий.
Мы используем описанный выше механизм с помещением данных в ТаблицуЗначений, передачей через временное хранилище и вызовом процедуры из общего модуля.
В регламентном задании у нас нет клиентского контекста и формы. Мы можем создать экземпляр нашей обработки на сервере и напрямую вызвать процедуру из ее модуля объекта (если мы хотим использовать ее контекст) или, что предпочтительнее для больших объемов данных, подготовить ТаблицуЗначений и настройки, а затем напрямую вызвать параметризованную процедуру из общего модуля.
Если в регламентном задании используется сама обработка (например, если она хранится в справочнике "ДополнительныеОтчетыИОбработки"), мы можем сделать так:
// В модуле регламентного задания или в процедуре, вызываемой регламентным заданием
Процедура ВыполнитьСозданиеНоменклатурыРегламентом() Экспорт
// 1. Создаем экземпляр обработки
Обработка = Обработки.МояОбработка.Создать();
// 2. Заполняем реквизиты обработки (настройки)
Обработка.ВидНоменклатурыПоУмолчанию = Справочники.ВидыНоменклатуры.НайтиПоКоду("00001");
// ... Заполняем другие настройки
// 3. Заполняем табличную часть обработки из какого-либо источника (например, запрос к базе)
// Обработка.ТЧ.Загрузить(ДанныеИзЗапроса);
// Или используем уже существующие данные
// 4. Выгружаем ТЧ обработки в ТаблицуЗначений для передачи в общий модуль
ТЗНоменклатураДляФона = Обработка.ТЧ.Выгрузить();
// 5. Формируем входные параметры для общего модуля
ПараметрыВхода = Новый Структура;
ПараметрыВхода.Вставить("ТаблицаНоменклатуры", ТЗНоменклатураДляФона);
ПараметрыВхода.Вставить("ВидНоменклатурыПоУмолчанию", Обработка.ВидНоменклатурыПоУмолчанию);
ПараметрыВхода.Вставить("ЕдиницаИзмеренияПоУмолчанию", Обработка.ВидНоменклатурыПоУмолчанию.ЕдиницаИзмерения);
// ...
// 6. Помещаем параметры во временное хранилище
АдресВходныхДанных = ПоместитьВоВременноеХранилище(ПараметрыВхода, УникальныйИдентификатор);
АдресВыходныхДанных = ПоместитьВоВременноеХранилище(Неопределено, УникальныйИдентификатор);
// 7. Вызываем процедуру из общего модуля напрямую
МояОбработкаБэкграунд.СоздатьНовуюНоменклатуруФоново(АдресВходныхДанных, АдресВыходныхДанных);
// 8. Получаем результаты из хранилища (если нужно для дальнейшей обработки в регламенте)
Результаты = ПолучитьИзВременногоХранилища(АдресВыходныхДанных);
Если Результаты <> Неопределено Тогда
// Обработка результатов регламентного задания
// Например, запись логов, отправка уведомлений
КонецЕсли;
КонецПроцедуры
В этом случае, хотя мы и используем временное хранилище (поскольку процедура СоздатьНовуюНоменклатуруФоново ожидает именно адреса), накладные расходы на него будут меньше, чем если бы мы запускали отдельное фоновое задание, так как здесь нет межпроцессного взаимодействия.
Важный момент: если ваша обработка может использоваться как в автоматическом режиме (регламентное задание), так и в ручном с возможностью фона, мы рекомендуем выносить всю основную логику работы с данными в общий модуль, используя параметризованные процедуры/функции. Это позволит избежать дублирования кода и обеспечит гибкость использования.
Как мы уже упоминали, у внешних обработок нет модуля менеджера. Если вы разрабатываете внешнюю обработку и хотите использовать механизм длительных операций, вам нужно будет:
"ВнешняяОбработка.МояВнешняяОбработка.МодульОбъекта.МояПроцедура".Выполнение длительных операций в 1С:Предприятии 8.3 с использованием фоновых заданий и временного хранилища является мощным инструментом для создания отзывчивого пользовательского интерфейса. Несмотря на необходимость рефакторинга кода и использования промежуточных структур данных (ТаблицаЗначений), этот подход позволяет эффективно обрабатывать большие объемы данных, не блокируя работу пользователя. Мы выяснили, что ключ к решению проблемы лежит в правильном использовании типов модулей, передаче данных через сериализуемые типы и адреса временного хранилища, а также в стандартизированных механизмах БСП для длительных операций.