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