Что такое ссылка на объект и как с ней работать в коде 1С

· 9 мин

Содержание

Ссылка на объект в 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 в полях связанной таблицы. Поэтому поля, полученные через точку, в фильтрах и арифметике всегда рассматривайте как потенциально пустые.

Соединять таблицы по ссылке можно и явно — это обязательно, когда нужно не левое, а внутреннее соединение, или когда условие соединения сложнее равенства:

ВЫБРАТЬ
    Заказы.Ссылка,
    Контрагенты.Наименование
ИЗ
    Документ.ЗаказПокупателя КАК Заказы
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
    ПО Заказы.Контрагент = Контрагенты.Ссылка

Сравнивать ссылки в условиях удобно через В ИЕРАРХИИ (для иерархических справочников) и литерал ЗНАЧЕНИЕ:

ГДЕ Товары.Группа В ИЕРАРХИИ (&ВыбраннаяГруппа)
    И Товары.Ставка = ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.БезНДС)

Ссылки как измерения регистров накопления

В регистре накопления ссылки обычно работают измерениями. Измерение — это разрез, в котором накапливаются ресурсы (количество, сумма). Типичный набор измерений регистра остатков товаров: Склад (ссылка на справочник складов), Номенклатура (ссылка), Организация (ссылка).

Что здесь важно про ссылки:

  1. 📊 Ресурсы — числовые, измерения — чаще ссылочные. Складывать можно только ресурсы. Ссылка в ресурс не кладётся: по ней нечего суммировать.
  2. 🧭 Регистратор — это ссылка на документ. Поле Регистратор имеет составной тип из всех документов-регистраторов регистра. Именно через него движения привязаны к документу, который их сформировал.
  3. 🔎 Виртуальные таблицы отдают ссылки как есть. Таблицы Остатки, Обороты, ОстаткиИОбороты возвращают значения измерений ссылками. Наименования складов и товаров из них получают через точку или соединением — так же, как в обычном запросе.

Пример выборки остатков с разыменованием измерения:

ВЫБРАТЬ
    Остатки.Номенклатура,
    Остатки.Номенклатура.Артикул КАК Артикул,
    Остатки.КоличествоОстаток
ИЗ
    РегистрНакопления.ТоварыНаСкладах.Остатки(, Склад = &Склад) КАК Остатки

Условие отбора по ссылочному измерению лучше задавать параметром виртуальной таблицы (в скобках после имени таблицы), а не в секции ГДЕ. Так отбор применяется раньше — на этапе чтения регистра, а не после, и запрос читает меньше данных.

Пустая ссылка в измерении — это полноценное значение, по которому тоже копятся остатки. Если в документе не заполнили склад, движения лягут на «пустой склад», и потом эти остатки будут мозолить глаза в отчётах. Поэтому заполненность ссылочных измерений стоит контролировать при проведении.

От ссылки к объекту: чтение и запись данных

Ссылки достаточно, чтобы прочитать данные запросом. Но чтобы изменить объект и сохранить изменения, нужен сам объект. Его возвращает метод ПолучитьОбъект():

Объект = Ссылка.ПолучитьОбъект();
Если Объект = Неопределено Тогда
    // объект уже удалён из базы
    Возврат;
КонецЕсли;
Объект.Комментарий = "Проверено";
Объект.Записать();

Разница между ссылкой и объектом в работе с данными принципиальна:

  • 👀 Ссылка — только для чтения. Через неё удобно разыменовывать реквизиты и сравнивать значения. Менять ничего нельзя.
  • ✏️ Объект — чтение и запись. Поднимает в память все реквизиты и табличные части, позволяет их править и записывать обратно методом Записать().

Поднимать объект только ради одного реквизита — расточительно. Если нужно просто прочитать значение, берите его запросом или разыменованием через точку, а ПолучитьОбъект() оставьте для случаев, когда объект действительно меняете.

При записи объекта платформа заново формирует движения по регистрам (если проведение включено), проверяет заполненность и запускает обработчики событий модуля объекта. Поэтому массовую правку лучше делать не в цикле по объектам, а взвесив, не решается ли задача документом-корректировкой или обработкой с пакетной записью в транзакции.

Блокировки тоже работают через объект: пока вы держите полученный объект и не записали его, другой сеанс не сможет изменить тот же элемент — платформа ставит объектную блокировку. Долго удерживать объект между интерактивными действиями пользователя не стоит.

Производительность: почему точка в цикле — это беда

Главное правило: обращение к реквизиту через точку вне запроса — это отдельное чтение из базы данных. Один раз — незаметно. В цикле по тысячам строк — это тысячи обращений к СУБД.

Антипример, который встречается постоянно:

Для Каждого СтрокаТЧ Из Документ.Товары Цикл
    // каждая точка ниже — чтение объекта номенклатуры из базы
    Артикул = СтрокаТЧ.Номенклатура.Артикул;
    Ставка  = СтрокаТЧ.Номенклатура.СтавкаНДС;
КонецЦикла;

Здесь на каждую строку платформа читает объект номенклатуры целиком (хотя закэширует его при повторном обращении к той же ссылке). Правильно — получить все нужные реквизиты одним запросом:

ВЫБРАТЬ
    Товары.Номенклатура,
    Товары.Номенклатура.Артикул КАК Артикул,
    Товары.Номенклатура.СтавкаНДС КАК Ставка
ИЗ
    Документ.РеализацияТоваровУслуг.Товары КАК Товары
ГДЕ Товары.Ссылка = &Ссылка

Короткий чек-лист по работе со ссылками без тормозов:

Чек-лист: ссылки без тормозов
Выполнено: 0 / 6

Ещё несколько приёмов:

  • 🧱 Индексы. Если по полю-ссылке часто идёт отбор (ГДЕ Контрагент = ...), включите для реквизита индексирование в конфигураторе. Это ускорит выборку, но слегка замедлит запись.
  • 🎯 ВЫРАЗИТЬ вместо множественных соединений. Приведение составного типа к одному конкретному убирает лишние LEFT JOIN.
  • 🧮 Сравнивайте ссылки, а не представления. Условие Контрагент = &Контрагент отрабатывает по идентификатору и использует индекс. Сравнение по наименованию (Контрагент.Наименование = ...) индекс не задействует и работает медленно.

Проверьте себя

Проверьте себя
Что хранит реквизит документа ссылочного типа?
Чем занят метод `ПустаяСсылка()`?
Во что разворачивается обращение `Документ.Контрагент.ИНН` в запросе?
Откуда в результате запроса берётся `NULL`?

Частые вопросы

Чем ссылка отличается от объекта?
Ссылка — это указатель: тип плюс идентификатор, без данных. Объект (получается через ПолучитьОбъект()) содержит реквизиты и табличные части и позволяет их менять и записывать. Для чтения в большинстве случаев достаточно ссылки и запроса — объект целиком поднимать не нужно.
Остаётся ли ссылка валидной после переименования элемента?
Да. Идентификатор присваивается один раз при записи и не меняется при смене наименования, кода или пометке на удаление. Поэтому ссылки в документах не «протухают» при редактировании справочника.
Как в запросе подставить конкретное значение перечисления или предопределённый элемент?
Через литерал ЗНАЧЕНИЕ(...): например, ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.БезНДС) или ЗНАЧЕНИЕ(Справочник.Валюты.Рубль). Параметр для этого не нужен, значение известно на этапе конфигурирования.
Почему сумма в отчёте получилась пустой, хотя данные есть?
Скорее всего, в арифметику попал NULL из левого соединения: любое действие с NULL даёт NULL. Оберните поля в ЕСТЬNULL(Поле, 0) перед сложением.
Можно ли по ссылке понять, существует ли ещё объект в базе?
Сама ссылка этого не гарантирует: ПолучитьСсылку по идентификатору вернёт ссылку, даже если объект удалён. Проверяйте существование запросом (есть ли строка с таким Ссылка) или через ПолучитьОбъект() с проверкой результата на Неопределено.