Что такое ссылка на объект и как с ней работать в коде 1С
Содержание
Ссылка на объект в 1С — это значение, которое однозначно указывает на конкретный элемент справочника, документ или другой объект базы, но не хранит его данные внутри себя. Физически это уникальный идентификатор плюс информация о типе. Когда вы кладёте в реквизит документа контрагента, вы кладёте туда не сам справочник целиком, а ссылку на него.
Разберём, из чего состоит ссылка, какие типы ссылок существуют, чем пустая ссылка отличается от NULL и Неопределено, как ссылки ведут себя в запросах и регистрах и почему обращение через точку в цикле убивает производительность.
Из чего состоит ссылка и как она лежит в базе
Внутри ссылка — это пара «тип объекта + уникальный идентификатор». Идентификатор представляет собой GUID длиной 16 байт, который платформа присваивает элементу один раз при записи и уже не меняет. Переименование элемента, смена кода, пометка на удаление — всё это не трогает идентификатор, поэтому ссылки на объект остаются валидными.
В таблицах СУБД ссылка хранится компактно:
- 🔑 поле вида
_IDRRef— бинарный идентификатор на 16 байт; - 🏷️ для полей составного типа рядом лежит дискриминатор типа (
_TYPE,_RTRef), чтобы платформа понимала, на объект какого вида указывает идентификатор.
Получить идентификатор программно можно методом УникальныйИдентификатор():
Ид = Ссылка.УникальныйИдентификатор(); // объект УникальныйИдентификатор
СтрокойДляЛога = Строка(Ид); // 32 шестнадцатеричных символа
Обратно восстановить ссылку по идентификатору помогает менеджер объекта:
Ид = Новый УникальныйИдентификатор("a1b2c3d4-...-...");
Ссылка = Справочники.Контрагенты.ПолучитьСсылку(Ид);
ПолучитьСсылку не проверяет, существует ли такой элемент на самом деле. Вернётся ссылка с нужным идентификатором, даже если объект уже удалён. Существование проверяйте отдельно — запросом или через ПолучитьОбъект() с проверкой на Неопределено.
Важно различать ссылку и объект. Ссылка — это только указатель, объект — это сами данные. Пока вы работаете со ссылкой, платформа не читает из базы реквизиты. Как только вы обращаетесь к реквизиту через точку или вызываете ПолучитьОбъект(), происходит чтение.
Какие бывают ссылочные типы данных
Ссылочный тип есть почти у каждого прикладного объекта, который хранит данные. Имя типа строится как «ИмяКлассаСсылка.ИмяОбъекта»:
| Класс объектов | Тип ссылки | Пример |
|---|---|---|
| Справочники | СправочникСсылка.<Имя> | СправочникСсылка.Контрагенты |
| Документы | ДокументСсылка.<Имя> | ДокументСсылка.РеализацияТоваровУслуг |
| Планы видов характеристик | ПланВидовХарактеристикСсылка.<Имя> | — |
| Планы счетов | ПланСчетовСсылка.<Имя> | ПланСчетовСсылка.Хозрасчетный |
| Планы видов расчёта | ПланВидовРасчетаСсылка.<Имя> | — |
| Перечисления | ПеречислениеСсылка.<Имя> | ПеречислениеСсылка.СтавкиНДС |
| Бизнес-процессы, задачи | БизнесПроцессСсылка, ЗадачаСсылка | — |
| Планы обмена | ПланОбменаСсылка.<Имя> | — |
Отдельно стоит составной ссылочный тип — когда у реквизита в конфигураторе задано несколько типов. Классический пример — измерение регистра «Регистратор», которое может указывать на документы разных видов. Значение такого реквизита в каждый момент имеет один конкретный тип, но на этапе проектирования поле «широкое».
Составной тип удобен, но дорог: по каждому возможному типу платформа хранит отдельную колонку идентификатора, а обращение через точку в запросе разворачивается в несколько соединений. Если типов в реквизите реально нужно два-три — это нормально. Если десять — пересмотрите структуру.
Проверить тип ссылки в коде можно так:
Если ТипЗнч(Ссылка) = Тип("СправочникСсылка.Контрагенты") Тогда
// это контрагент
КонецЕсли;
Пустая ссылка, NULL и Неопределено — в чём разница
Это три разных «ничего», и путаница между ними — источник доброй половины ошибок у начинающих.
Пустая ссылка — это ссылка с типом, но с нулевым идентификатором. Её возвращает менеджер объекта:
ПустойКонтрагент = Справочники.Контрагенты.ПустаяСсылка();
Пустая ссылка знает свой тип. Незаполненный реквизит ссылочного типа содержит именно пустую ссылку, а не Неопределено. Проверить, пустая ли ссылка, можно двумя способами:
Если Ссылка.Пустая() Тогда ... // метод самой ссылки
Если НЕ ЗначениеЗаполнено(Ссылка) Тогда ... // универсальная проверка
NULL появляется в результатах запросов, когда соединение не нашло пару. Левое соединение со справочником дало строку, а связанного элемента нет — в полях этого справочника будет NULL. Также NULL приходит из полей, которых физически нет (например, обращение к несуществующему реквизиту через точку по составному типу).
Неопределено — это «значение не задано вообще». Его возвращает, например, метод Найти... коллекции, когда ничего не нашёл, или необязательный параметр функции без значения.
Таблица для памяти:
| Значение | Откуда берётся | Как проверить |
|---|---|---|
| Пустая ссылка | незаполненный реквизит ссылочного типа | Ссылка.Пустая(), ЗначениеЗаполнено() |
NULL | левое соединение без пары, отсутствующее поле запроса | ЕСТЬNULL(Поле, ...) в запросе, Поле = NULL |
Неопределено | несработавший поиск, незаданный параметр | Значение = Неопределено |
В запросах оборачивайте потенциально пустые поля в ЕСТЬNULL(Поле, 0) или ЕСТЬNULL(Поле, ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)). Иначе NULL в арифметике «заразит» всю сумму и даст NULL на выходе.
Ссылки в запросах: разыменование и автосоединения
В языке запросов к реквизитам связанного объекта обращаются через точку:
ВЫБРАТЬ
Продажи.Контрагент,
Продажи.Контрагент.ИНН КАК ИНН,
Продажи.Контрагент.ОсновнойМенеджер.Наименование КАК Менеджер
ИЗ
Документ.РеализацияТоваровУслуг КАК Продажи
Каждая такая точка — это автоматическое левое соединение (LEFT JOIN) с таблицей соответствующего справочника. Платформа добавляет его сама. Удобно, но есть две ловушки.
Ловушка первая — составные типы. Если поле Контрагент имело бы составной тип, обращение Контрагент.ИНН развернулось бы в соединение с каждой таблицей из состава типа. Чтобы оставить одно соединение, явно приводите тип:
ВЫРАЗИТЬ(Продажи.Контрагент КАК Справочник.Контрагенты).ИНН
Ловушка вторая — цепочки через несколько точек. Запись Контрагент.ОсновнойМенеджер.ФизическоеЛицо.Наименование создаёт цепочку соединений. На больших таблицах это заметно бьёт по плану запроса. Когда реквизит нужен часто, иногда дешевле хранить его копию в самом документе.
Левое соединение по ссылке на несуществующий элемент даст NULL в полях связанной таблицы. Поэтому поля, полученные через точку, в фильтрах и арифметике всегда рассматривайте как потенциально пустые.
Соединять таблицы по ссылке можно и явно — это обязательно, когда нужно не левое, а внутреннее соединение, или когда условие соединения сложнее равенства:
ВЫБРАТЬ
Заказы.Ссылка,
Контрагенты.Наименование
ИЗ
Документ.ЗаказПокупателя КАК Заказы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
ПО Заказы.Контрагент = Контрагенты.Ссылка
Сравнивать ссылки в условиях удобно через В ИЕРАРХИИ (для иерархических справочников) и литерал ЗНАЧЕНИЕ:
ГДЕ Товары.Группа В ИЕРАРХИИ (&ВыбраннаяГруппа)
И Товары.Ставка = ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.БезНДС)
Ссылки как измерения регистров накопления
В регистре накопления ссылки обычно работают измерениями. Измерение — это разрез, в котором накапливаются ресурсы (количество, сумма). Типичный набор измерений регистра остатков товаров: Склад (ссылка на справочник складов), Номенклатура (ссылка), Организация (ссылка).
Что здесь важно про ссылки:
- 📊 Ресурсы — числовые, измерения — чаще ссылочные. Складывать можно только ресурсы. Ссылка в ресурс не кладётся: по ней нечего суммировать.
- 🧭 Регистратор — это ссылка на документ. Поле
Регистраторимеет составной тип из всех документов-регистраторов регистра. Именно через него движения привязаны к документу, который их сформировал. - 🔎 Виртуальные таблицы отдают ссылки как есть. Таблицы
Остатки,Обороты,ОстаткиИОборотывозвращают значения измерений ссылками. Наименования складов и товаров из них получают через точку или соединением — так же, как в обычном запросе.
Пример выборки остатков с разыменованием измерения:
ВЫБРАТЬ
Остатки.Номенклатура,
Остатки.Номенклатура.Артикул КАК Артикул,
Остатки.КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки
Условие отбора по ссылочному измерению лучше задавать параметром виртуальной таблицы (в скобках после имени таблицы), а не в секции ГДЕ. Так отбор применяется раньше — на этапе чтения регистра, а не после, и запрос читает меньше данных.
Пустая ссылка в измерении — это полноценное значение, по которому тоже копятся остатки. Если в документе не заполнили склад, движения лягут на «пустой склад», и потом эти остатки будут мозолить глаза в отчётах. Поэтому заполненность ссылочных измерений стоит контролировать при проведении.
От ссылки к объекту: чтение и запись данных
Ссылки достаточно, чтобы прочитать данные запросом. Но чтобы изменить объект и сохранить изменения, нужен сам объект. Его возвращает метод ПолучитьОбъект():
Объект = Ссылка.ПолучитьОбъект();
Если Объект = Неопределено Тогда
// объект уже удалён из базы
Возврат;
КонецЕсли;
Объект.Комментарий = "Проверено";
Объект.Записать();
Разница между ссылкой и объектом в работе с данными принципиальна:
- 👀 Ссылка — только для чтения. Через неё удобно разыменовывать реквизиты и сравнивать значения. Менять ничего нельзя.
- ✏️ Объект — чтение и запись. Поднимает в память все реквизиты и табличные части, позволяет их править и записывать обратно методом
Записать().
Поднимать объект только ради одного реквизита — расточительно. Если нужно просто прочитать значение, берите его запросом или разыменованием через точку, а ПолучитьОбъект() оставьте для случаев, когда объект действительно меняете.
При записи объекта платформа заново формирует движения по регистрам (если проведение включено), проверяет заполненность и запускает обработчики событий модуля объекта. Поэтому массовую правку лучше делать не в цикле по объектам, а взвесив, не решается ли задача документом-корректировкой или обработкой с пакетной записью в транзакции.
Блокировки тоже работают через объект: пока вы держите полученный объект и не записали его, другой сеанс не сможет изменить тот же элемент — платформа ставит объектную блокировку. Долго удерживать объект между интерактивными действиями пользователя не стоит.
Производительность: почему точка в цикле — это беда
Главное правило: обращение к реквизиту через точку вне запроса — это отдельное чтение из базы данных. Один раз — незаметно. В цикле по тысячам строк — это тысячи обращений к СУБД.
Антипример, который встречается постоянно:
Для Каждого СтрокаТЧ Из Документ.Товары Цикл
// каждая точка ниже — чтение объекта номенклатуры из базы
Артикул = СтрокаТЧ.Номенклатура.Артикул;
Ставка = СтрокаТЧ.Номенклатура.СтавкаНДС;
КонецЦикла;
Здесь на каждую строку платформа читает объект номенклатуры целиком (хотя закэширует его при повторном обращении к той же ссылке). Правильно — получить все нужные реквизиты одним запросом:
ВЫБРАТЬ
Товары.Номенклатура,
Товары.Номенклатура.Артикул КАК Артикул,
Товары.Номенклатура.СтавкаНДС КАК Ставка
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК Товары
ГДЕ Товары.Ссылка = &Ссылка
Короткий чек-лист по работе со ссылками без тормозов:
Ещё несколько приёмов:
- 🧱 Индексы. Если по полю-ссылке часто идёт отбор (
ГДЕ Контрагент = ...), включите для реквизита индексирование в конфигураторе. Это ускорит выборку, но слегка замедлит запись. - 🎯
ВЫРАЗИТЬвместо множественных соединений. Приведение составного типа к одному конкретному убирает лишниеLEFT JOIN. - 🧮 Сравнивайте ссылки, а не представления. Условие
Контрагент = &Контрагентотрабатывает по идентификатору и использует индекс. Сравнение по наименованию (Контрагент.Наименование = ...) индекс не задействует и работает медленно.
Проверьте себя
Частые вопросы
Чем ссылка отличается от объекта?
ПолучитьОбъект()) содержит реквизиты и табличные части и позволяет их менять и записывать. Для чтения в большинстве случаев достаточно ссылки и запроса — объект целиком поднимать не нужно.Остаётся ли ссылка валидной после переименования элемента?
Как в запросе подставить конкретное значение перечисления или предопределённый элемент?
ЗНАЧЕНИЕ(...): например, ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.БезНДС) или ЗНАЧЕНИЕ(Справочник.Валюты.Рубль). Параметр для этого не нужен, значение известно на этапе конфигурирования.Почему сумма в отчёте получилась пустой, хотя данные есть?
NULL из левого соединения: любое действие с NULL даёт NULL. Оберните поля в ЕСТЬNULL(Поле, 0) перед сложением.Можно ли по ссылке понять, существует ли ещё объект в базе?
ПолучитьСсылку по идентификатору вернёт ссылку, даже если объект удалён. Проверяйте существование запросом (есть ли строка с таким Ссылка) или через ПолучитьОбъект() с проверкой результата на Неопределено.