В процессе разработки в среде «1С:Предприятие 8» программисты часто сталкиваются с проблемой появления в базе данных так называемых «битых» ссылок. Это происходит, когда объект (например, документ или элемент справочника) физически удаляется из базы данных, но в других объектах или регистрах сохраняются ссылки на него. В интерфейсе пользователя такие ссылки отображаются в виде некрасивой строки <Объект не найден> (уникальный идентификатор).
В этой статье мы подробно разберем все существующие способы проверки ссылок на существование, сравним их эффективность, проанализируем влияние прав доступа и выберем наиболее оптимальные подходы для различных сценариев разработки.
Этот метод считается классическим, наиболее надежным и правильным с точки зрения архитектуры СУБД. Мы обращаемся к таблице базы данных и проверяем, есть ли в ней запись с нашей ссылкой. Если запрос возвращает пустой результат, значит, физического объекта в базе нет.
Рассмотрим пример функции, которая динамически определяет имя таблицы метаданных и выполняет точечный запрос к базе данных:
Функция СсылкаСуществует(ЛюбаяСсылка) Экспорт
// Проверяем, что ссылка заполнена и имеет корректный тип
Если НЕ ЗначениеЗаполнено(ЛюбаяСсылка) Тогда
Возврат Ложь;
КонецЕсли;
ТекстЗапроса = "
|ВЫБРАТЬ
| Ссылка КАК Ссылка
|ИЗ
| [ИмяТаблицы]
|ГДЕ
| Ссылка = &Ссылка
|";
// Подставляем полное имя таблицы (например, Справочник.Номенклатура)
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "[ИмяТаблицы]", ЛюбаяСсылка.Метаданные().ПолноеИмя());
Запрос = Новый Запрос;
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("Ссылка", ЛюбаяСсылка);
Возврат НЕ Запрос.Выполнить().Пустой();
КонецФункции
Преимущества: этот метод работает чрезвычайно быстро на уровне СУБД, так как поиск происходит по первичному ключу (кластерному индексу), и база данных не считывает лишних полей.
Проанализируем ситуацию, когда у пользователя настроены ограничения прав на уровне записей (RLS). Если выполнять указанный выше запрос под сеансом пользователя с ограниченными правами, то для объектов, на чтение которых у него нет прав, запрос вернет пустой результат. В итоге программа ошибочно посчитает существующую ссылку «битой».
Решение: Для проверки физического существования записи в БД код обязательно должен выполняться в привилегированном режиме. Оберните вызов функции в привилегированный общий модуль или принудительно включите привилегированный режим на время выполнения проверки:
УстановитьПривилегированныйРежим(Истина);
Существует = СсылкаСуществует(ПроверяемаяСсылка);
УстановитьПривилегированныйРежим(Ложь);
Если вы ведете разработку в конфигурации, созданной на основе Библиотеки стандартных подсистем (БСП), то изобретать велосипед не нужно. В составе БСП уже есть готовые, оптимизированные функции в общем модуле ОбщегоНазначения.
Рассмотрим два основных инструмента:
ОбщегоНазначения.СсылкаСуществует(ПроверяемаяСсылка) — выполняет быстрый запрос в привилегированном режиме и возвращает булево значение. Это готовый аналог метода, описанного выше.ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Ссылка, ИмяРеквизита) — если вам требуется не просто проверить факт существования объекта, но и получить его реквизит (например, проверить, помечен ли он на удаление), используйте эту функцию. Если ссылка окажется битой, функция не вызовет критической ошибки платформы, а вернет значение Неопределено. При этом платформа не будет считывать в кэш весь объект целиком, что экономит оперативную память сервера.Простой способ в одну строку, который часто используют начинающие разработчики:
ЕстьОбъект = (Ссылка.ПолучитьОбъект() <> Неопределено);
Почему этот способ нежелателен в высоконагруженных системах? Метод ПолучитьОбъект() считывает из СУБД абсолютно все реквизиты объекта, включая все его табличные части, и помещает их в оперативную память. Если вы проверяете тяжелый многострочный документ или большой справочник, это создает колоссальную избыточную нагрузку на сервер и дисковую подсистему.
Вариант, позволяющий избежать явного написания запросов:
ЕстьОбъект = ЗначениеЗаполнено(Ссылка.ВерсияДанных);
Особенности работы: При обращении к свойству ВерсияДанных «через точку» платформа выполняет неявное считывание объекта. По производительности этот метод практически идентичен методу ПолучитьОбъект() и уступает специализированному точечному запросу.
В процессе обсуждения часто всплывает «лайфхак» с поиском подстроки в строковом представлении объекта:
ЕстьОбъект = (Найти(Строка(Ссылка), "<Объект не") = 0);
Несмотря на то, что замеры на файловой базе могут показывать высокую скорость выполнения этой операции, применять такой метод в промышленной разработке категорически запрещено по следующим причинам:
"Object not found", в немецком — "Objekt nicht gefunden". Код, завязанный на жесткую строковую константу, мгновенно перестанет работать при смене языка интерфейса или платформы.Иногда на просторах интернета можно встретить рекомендацию использовать конструкцию:
Попытка
Тест = Ссылка.Ссылка;
Исключение
// Объект не найден
КонецПопытки
Важно знать: Этот способ не работает. Обращение к свойству Ссылка у битой ссылки просто возвращает эту же ссылку и никогда не вызывает исключения платформы. Проверка всегда будет проходить успешно, даже если объекта физически не существует в базе данных.
Если перед вами стоит задача проверить массив из сотен или тысяч ссылок, выполнять точечные запросы в цикле — грубейшая ошибка, которая приведет к зависанию системы. В таких случаях необходимо использовать пакетный запрос.
Рассмотрим алгоритм групповой проверки:
// Предположим, у нас есть МассивСсылокРазныхТипов
// Шаг 1. Группируем ссылки по типам метаданных для оптимизации запросов
ТаблицаПоТипам = Новый Соответствие;
Для Каждого СсылкаИзМассива Из МассивСсылокРазныхТипов Цикл
ТипОбъекта = СсылкаИзМассива.Метаданные();
МассивДляТипа = ТаблицаПоТипам.Получить(ТипОбъекта);
Если МассивДляТипа = Неопределено Тогда
МассивДляТипа = Новый Массив;
ТаблицаПоТипам.Вставить(ТипОбъекта, МассивДляТипа);
КонецЕсли;
МассивДляТипа.Добавить(СсылкаИзМассива);
КонецЦикла;
// Шаг 2. Выполняем запросы по каждому типу метаданных
СуществующиеСсылки = Новый Массив;
Для Каждого КлючИЗначение Из ТаблицаПоТипам Цикл
МетаданныеТипа = КлючИЗначение.Ключ;
МассивКонкретныхСсылок = КлючИЗначение.Значение;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Таб.Ссылка КАК Ссылка
|ИЗ
| " + МетаданныеТипа.ПолноеИмя() + " КАК Таб
|ГДЕ
| Таб.Ссылка В (&МассивСсылок)";
Запрос.УстановитьПараметр("МассивСсылок", МассивКонкретныхСсылок);
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
СуществующиеСсылки.Добавить(Выборка.Ссылка);
КонецЦикла;
КонецЦикла;
// В результате в массиве "СуществующиеСсылки" останутся только «живые» объекты
Если вы пишете сложный отчет или обработку и хотите прямо в основном запросе отсечь несуществующие элементы (например, битые ссылки в измерениях регистра), используйте левое соединение или обращение к реквизиту через точку в комбинации с проверкой на NULL.
Посмотрим на пример условия в запросе:
ГДЕ
НЕ ТаблицаПродаж.Номенклатура = ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка)
И ТаблицаПродаж.Номенклатура.Ссылка ЕСТЬ NULL
Как это работает: Если номенклатура заполнена (не пустая ссылка), но при этом обращение к реквизиту Номенклатура.Ссылка возвращает значение NULL, это однозначно свидетельствует о том, что объект был физически удален из базы данных, то есть ссылка является «битой».