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