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