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