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