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