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