Как передать количество обработанных элементов с сервера на клиент и вывести сообщение в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При разработке в управляемом приложении 1С 8.3 программисты часто сталкиваются с необходимостью выполнять тяжелые операции обработки данных на сервере, а затем информировать пользователя о результатах на клиенте. Клиент-серверная архитектура строго разграничивает контекст выполнения кода: клиентский сеанс отвечает за интерфейс и взаимодействие с пользователем, а серверный — за непосредственную работу с базой данных, выполнение запросов и проведение документов.

В этой статье мы подробно разберем, как правильно передавать данные о результатах работы серверных процедур на клиент, рассмотрим классические способы вывода сообщений и выявим критические ошибки производительности, которые часто допускают начинающие разработчики при работе с циклами и базами данных.

Анализ исходной ситуации

Рассмотрим код, представленный в обсуждении. Задача состоит в том, чтобы выбрать элементы справочника Номенклатура, проверить их наличие в регистре сведений «Установка соответствия номенклатуры и статей затрат» и, если таких записей еще нет, добавить их в регистр. В конце работы необходимо сообщить пользователю, сколько именно новых записей было успешно добавлено.

Ниже приведен исходный вариант кода, содержащий как методические ошибки работы с контекстом, так и серьезные проблемы с производительностью системы:


&НаСервереБезКонтекста
Процедура ЗаполнитьИзСпрНоменклатураНаСервере()
    ОсновнаяОрганизация = ПользователиБКВызовСервераПовтИсп.ПолучитьЗначениеПоУмолчанию(Пользователи.ТекущийПользователь, "ОсновнаяОрганизация");
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Номенклатура.Ссылка КАК Номенклатура,
        |	Номенклатура.СтатьяЗатрат,
        |	Номенклатура.СтатьяЗатрат.СтатьяЗакупаСнабжения КАК СтатьяЗакупаСнабжения,
        |	Номенклатура.СтатьяЗатрат.СтатьяЗакупаСнабжения.ЦелиПоступленияВыбытия КАК ЦелиПоступленияВыбытия,
        |	Номенклатура.СтатьяЗатрат.СтатьяЗакупаСнабжения.ПодстатьяПритокаОттока КАК ПодстатьяПритокаОттока
        |ИЗ
        |	Справочник.Номенклатура КАК Номенклатура
        |ГДЕ
        |	Номенклатура.ЭтоГруппа = ЛОЖЬ
        |	И Номенклатура.ПометкаУдаления = ЛОЖЬ
        |	И Номенклатура.Наименование <> &НаимНоменклатура
        |	И Номенклатура.СтатьяЗатрат.Наименование <> &НаимСтатьяЗатрат
        |УПОРЯДОЧИТЬ ПО
        |	Номенклатура";
        
    Запрос.УстановитьПараметр("НаимНоменклатура", "");
    Запрос.УстановитьПараметр("НаимСтатьяЗатрат", "");
    
    Результат = Запрос.Выполнить().Выбрать();
    ДобавленоЭлементов = 0;
    
    Пока Результат.Следующий() Цикл
        // КРИТИЧЕСКАЯ ОШИБКА: Запрос в цикле!
        ЗапросРегистра = Новый Запрос;
        ЗапросРегистра.Текст = 
            "ВЫБРАТЬ
            |	УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат.Номенклатура
            |ИЗ
            |	РегистрСведений.УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат КАК УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат
            |ГДЕ
            |	УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат.Номенклатура = &Номенклатура
            |	И УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат.Организация = &Организация";
            
        ЗапросРегистра.УстановитьПараметр("Номенклатура", Результат.Номенклатура);
        ЗапросРегистра.УстановитьПараметр("Организация", ОсновнаяОрганизация);
        
        ЧистыйРегистр = ЗапросРегистра.Выполнить();
        
        Если ЧистыйРегистр.Пустой() Тогда
            // Лишний вызов тяжелой функции внутри цикла
            ОсновнаяОрганизация = ПользователиБКВызовСервераПовтИсп.ПолучитьЗначениеПоУмолчанию(Пользователи.ТекущийПользователь, "ОсновнаяОрганизация");
            
            // Запись набора записей на каждом шаге цикла — крайне медленно!
            ЗаписьРегистра = РегистрыСведений.УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат.СоздатьНаборЗаписей();
            ЗаписьРегистра.Отбор.Номенклатура.Установить(Результат.Номенклатура);
            
            мЗаписьРегистра = ЗаписьРегистра.Добавить();
            мЗаписьРегистра.Организация = ОсновнаяОрганизация;
            мЗаписьРегистра.Номенклатура = Результат.Номенклатура;
            мЗаписьРегистра.СтатьяЗатрат = Результат.СтатьяЗатрат;
            мЗаписьРегистра.Подразделение = Справочники.ПодразделенияОрганизаций.ПустаяСсылка();
            мЗаписьРегистра.СтатьиЗакупаСнабжения = Результат.СтатьяЗакупаСнабжения;
            мЗаписьРегистра.ПодстатьяПритокаОттока = Результат.ПодстатьяПритокаОттока;
            мЗаписьРегистра.ЦельПоступленияВыбытия = Результат.ЦелиПоступленияВыбытия;
            мЗаписьРегистра.Исключение = Ложь;
            мЗаписьРегистра.Период = ТекущаяДата();
            
            ЗаписьРегистра.Записать();
            ДобавленоЭлементов = ДобавленоЭлементов + 1;
        КонецЕсли;
    КонецЦикла;
КонецПроцедуры

&НаКлиенте
Процедура ЗаполнитьИзСпрНоменклатура(Команда)
    ЗаполнитьИзСпрНоменклатураНаСервере();
    // Как вывести сообщение с переменной ДобавленоЭлементов?
КонецПроцедуры

Проанализируем сначала непосредственное решение задачи вывода сообщения, а затем разберем, как сделать этот алгоритм быстрым и безопасным для базы данных.

Решение задачи: Передача данных на клиент и вывод сообщения

В управляемом приложении переменные, объявленные на сервере, недоступны на клиенте напрямую. Для передачи значения переменной ДобавленоЭлементов на клиент существует несколько стандартных и эффективных подходов.

Вариант 1. Преобразование процедуры в функцию (Рекомендуемый метод)

Самый простой и логичный способ передать числовые или текстовые данные обратно на клиент — это переписать серверную процедуру в функцию и вернуть значение с помощью ключевого слова Возврат.

Посмотрим, как изменится код вызова:


&НаСервереБезКонтекста
Функция ЗаполнитьИзСпрНоменклатураНаСервере()
    // [Код выполнения алгоритма заполнения]
    
    // В конце функции возвращаем накопленный результат
    Возврат ДобавленоЭлементов;
КонецФункции

&НаКлиенте
Процедура ЗаполнитьИзСпрНоменклатура(Команда)
    // Получаем значение напрямую из серверной функции
    КоличествоДобавленных = ЗаполнитьИзСпрНоменклатураНаСервере();
    
    // Формируем и выводим сообщение пользователю
    ТекстСообщения = "Заполнение завершено. Добавлено элементов: " + КоличествоДобавленных;
    ПоказатьПредупреждение(, ТекстСообщения);
КонецПроцедуры

Вариант 2. Использование объекта СообщениеПользователю на сервере

Многие начинающие разработчики считают, что объект СообщениеПользователю доступен только на клиенте. Однако это не так. Платформа 1С позволяет инициализировать и вызывать этот объект непосредственно в серверном коде.

Платформа собирает все сообщения, сформированные на сервере во время серверного вызова, кэширует их и передает на клиентский уровень в момент возврата управления. Пользователь увидит их на экране в стандартном окне сообщений.

Реализуем этот вариант в конце серверной процедуры:


    // Конец серверной процедуры заполнения
    Сообщение = Новый СообщениеПользователю;
    Сообщение.Текст = "Добавлено элементов: " + ДобавленоЭлементов;
    Сообщение.Сообщить();
КонецПроцедуры

Если в конфигурации используется Библиотека стандартных подсистем (БСП), то согласно стандартам разработки 1С рекомендуется использовать готовые методы общего модуля:


ОбщегоНазначенияКлиентСервер.СообщитьПользователю("Добавлено элементов: " + ДобавленоЭлементов);

Вариант 3. Современный UX через всплывающие оповещения

Использование классического окна сообщений или модальных предупреждений может перегружать интерфейс. Более современный подход с точки зрения юзабилити — использование асинхронных всплывающих окон, которые не блокируют работу пользователя и автоматически исчезают через несколько секунд.

Для этого мы можем использовать функцию на сервере для получения числа добавленных строк, а на клиенте вызвать метод ПоказатьОповещениеПользователя:


&НаКлиенте
Процедура ЗаполнитьИзСпрНоменклатура(Команда)
    КоличествоДобавленных = ЗаполнитьИзСпрНоменклатураНаСервере();
    
    Если КоличествоДобавленных > 0 Тогда
        ПоказатьОповещениеПользователя(
            "Успешно выполнено",
            ,
            "Добавлено новых соответствий: " + КоличествоДобавленных,
            БиблиотекаКартинок.Информация
        );
    Иначе
        ПоказатьОповещениеПользователя(
            "Информация",
            ,
            "Новых элементов для добавления не обнаружено",
            БиблиотекаКартинок.Предупреждение
        );
    КонецЕсли;
КонецПроцедуры

---

Оптимизация производительности: Устранение критических ошибок

Даже если мы настроим вывод сообщений, исходный код серверной части будет работать крайне медленно на реальных базах данных из-за грубых ошибок проектирования. Проанализируем эти проблемы и исправим их.

1. Антипаттерн «Запрос в цикле»
Внутри цикла по номенклатуре на каждом шаге выполняется новый запрос к регистру сведений. Если в справочнике номенклатуры содержится 10 000 позиций, система выполнит 10 000 отдельных запросов к СУБД. Это создает колоссальную нагрузку на сеть и сервер базы данных. Мы должны полностью отказаться от этой конструкции в пользу **ЛЕВОГО СОЕДИНЕНИЯ** в основном запросе.

2. Запись в базу данных внутри цикла
Метод Записать() для набора записей вызывается внутри цикла для каждой отдельной номенклатуры. Каждая операция записи в 1С инициирует открытие и закрытие транзакции базы данных. Запись 1000 элементов по одному займет в десятки раз больше времени, чем запись этих же 1000 элементов одним пакетом. Мы должны оптимизировать этот процесс.

3. Лишние вызовы функций в цикле
Вызов получения значения по умолчанию для организации ПолучитьЗначениеПоУмолчанию() дублируется внутри условия проверки. Эта функция обращается к кэшированным данным, но регулярный ее вызов внутри цикла создает неоправданные накладные расходы. Нам достаточно получить это значение один раз до начала выполнения цикла.

---

Разрабатываем оптимизированный и быстрый алгоритм

Перепишем весь серверный код в соответствии со стандартами разработки 1С. Мы свяжем справочник номенклатуры и регистр соответствий с помощью левого соединения в запросе. Таким образом, база данных сразу вернет нам только те позиции номенклатуры, по которым в регистре еще нет записей.

Рассмотрим финальный, чистый и производительный вариант реализации:


&НаСервереБезКонтекста
Функция ЗаполнитьИзСпрНоменклатураНаСервере()
    
    // 1. Получаем организацию ОДИН раз до начала всех операций
    ТекущийПользователь = Пользователи.ТекущийПользователь();
    ОсновнаяОрганизация = ПользователиБКВызовСервераПовтИсп.ПолучитьЗначениеПоУмолчанию(ТекущийПользователь, "ОсновнаяОрганизация");
    
    // 2. Пишем один оптимальный запрос с левым соединением
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Номенклатура.Ссылка КАК Номенклатура,
        |	Номенклатура.СтатьяЗатрат КАК СтатьяЗатрат,
        |	Номенклатура.СтатьяЗатрат.СтатьяЗакупаСнабжения КАК СтатьяЗакупаСнабжения,
        |	Номенклатура.СтатьяЗатрат.СтатьяЗакупаСнабжения.ЦелиПоступленияВыбытия КАК ЦелиПоступленияВыбытия,
        |	Номенклатура.СтатьяЗатрат.СтатьяЗакупаСнабжения.ПодстатьяПритокаОттока КАК ПодстатьяПритокаОттока
        |ИЗ
        |	Справочник.Номенклатура КАК Номенклатура
        |	// Левое соединение позволяет отсечь уже существующие в регистре записи
        |	ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат КАК Соответствие
        |		ПО Соответствие.Номенклатура = Номенклатура.Ссылка
        |		И Соответствие.Организация = &Организация
        |ГДЕ
        |	Номенклатура.ЭтоГруппа = ЛОЖЬ
        |	И Номенклатура.ПометкаУдаления = ЛОЖЬ
        |	И Номенклатура.Наименование <> &ПустоеНаименование
        |	И Соответствие.Номенклатура ЕСТЬ NULL // Выбираем только те, которых НЕТ в регистре
        |УПОРЯДОЧИТЬ ПО
        |	Номенклатура";
        
    Запрос.УстановитьПараметр("Организация", ОсновнаяОрганизация);
    Запрос.УстановитьПараметр("ПустоеНаименование", "");
    
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        Возврат 0; // Сразу выходим, если нечего добавлять
    КонецЕсли;
    
    Выборка = РезультатЗапроса.Выбрать();
    ДобавленоЭлементов = 0;
    
    // 3. Открываем явную транзакцию для ускорения записи в базу данных
    НачатьТранзакцию();
    
    Попытка
        Пока Выборка.Следующий() Цикл
            
            // Создаем набор записей для конкретной номенклатуры
            НаборЗаписей = РегистрыСведений.УстановкаСоответсвияНоменклатуры_и_СтатьиЗатрат.СоздатьНаборЗаписей();
            НаборЗаписей.Отбор.Номенклатура.Установить(Выборка.Номенклатура);
            НаборЗаписей.Отбор.Организация.Установить(ОсновнаяОрганизация);
            
            НоваяЗапись = НаборЗаписей.Добавить();
            НоваяЗапись.Организация = ОсновнаяОрганизация;
            НоваяЗапись.Номенклатура = Выборка.Номенклатура;
            НоваяЗапись.СтатьяЗатрат = Выборка.СтатьяЗатрат;
            НоваяЗапись.Подразделение = Справочники.ПодразделенияОрганизаций.ПустаяСсылка();
            НоваяЗапись.СтатьиЗакупаСнабжения = Выборка.СтатьяЗакупаСнабжения;
            НоваяЗапись.ПодстатьяПритокаОттока = Выборка.ПодстатьяПритокаОттока;
            НоваяЗапись.ЦельПоступленияВыбытия = Выборка.ЦелиПоступленияВыбытия;
            НоваяЗапись.Исключение = Ложь;
            НоваяЗапись.Период = ТекущаяДата();
            
            // Запись происходит быстро, так как мы находимся внутри одной общей транзакции
            НаборЗаписей.Записать(Истина);
            
            ДобавленоЭлементов = ДобавленоЭлементов + 1;
        КонецЦикла;
        
        // Фиксируем транзакцию — все данные сохраняются в БД одновременно
        ЗафиксироватьТранзакцию();
        
    Исключение
        // В случае любой непредвиденной ошибки откатываем изменения в исходное состояние
        ОтменитьТранзакцию();
        ВызватьИсключение КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
    КонецПопытки;
    
    Возврат ДобавленоЭлементов;
КонецФункции

&НаКлиенте
Процедура ЗаполнитьИзСпрНоменклатура(Команда)
    
    // Вызываем оптимизированную функцию
    КоличествоДобавленных = ЗаполнитьИзСпрНоменклатураНаСервере();
    
    // Красиво сообщаем пользователю о результате работы алгоритма
    Если КоличествоДобавленных > 0 Тогда
        ПоказатьОповещениеПользователя(
            "Заполнение успешно завершено",
            ,
            "Добавлено новых элементов: " + КоличествоДобавленных,
            БиблиотекаКартинок.Информация
        );
    Иначе
        ПоказатьОповещениеПользователя(
            "Процесс завершен",
            ,
            "Все соответствия уже заполнены ранее",
            БиблиотекаКартинок.Важно
        );
    КонецЕсли;
    
КонецПроцедуры

Что изменилось в коде и почему это важно?

  1. Скорость работы выросла в десятки раз. Мы избавились от тысяч мелких запросов к СУБД. Теперь выполняется ровно один легкий запрос к базе данных, который за доли секунды находит нужные дельты изменений.
  2. Безопасность данных повысилась. Использование конструкции НачатьТранзакцию() ... ЗафиксироватьТранзакцию() гарантирует, что если в процессе записи возникнет непредвиденный сбой (например, отключится питание или пропадет связь с сервером), база данных не останется в наполовину заполненном состоянии — транзакция полностью откатится назад.
  3. Код стал проще поддерживать. Мы убрали дублирование вызовов функций получения реквизитов организации и выстроили понятную логическую цепочку от интерфейса до работы с базой данных.
  4. Удобство для конечного пользователя. Всплывающее уведомление ПоказатьОповещениеПользователя плавно появляется на экране и не заставляет пользователя лишний раз нажимать кнопку «ОК» для закрытия диалоговых окон.
← На главную