В практике администрирования и разработки на платформе 1С:Предприятие иногда возникает пугающая ситуация: при попытке удалить справочник или документ (например, Договор контрагента) обработка удаления сообщает о наличии ссылок на этот объект. Однако при проверке через НайтиПоСсылкам в таблице результатов мы видим странную запись: объект не имеет представления, даты и номера, а его уникальный идентификатор (GUID) состоит из одних нулей. Стандартные процедуры тестирования и исправления (ТИИ) часто не находят ошибок, а удаление помеченных объектов завершается аварийно.
Разберем природу этой ошибки, выясним, где прячутся такие ссылки, и создадим алгоритм решения проблемы.
Давайте проанализируем, как платформа хранит данные и почему она показывает "пустоту".
В 99% случаев, когда вы видите в результатах поиска ссылок объект вида <Объект не найден> (0:00000000000000000000000000000000) или просто пустое представление типа документа (например, "Авансовый отчет" без номера и даты), это означает следующее:
NULL на уровне SQL или "Пустая ссылка" на уровне логики 1С.Такие записи-сироты часто появляются в результате некорректного обмена данными (когда установлен режим ОбменДанными.Загрузка = Истина), прямых манипуляций в SQL или сбоев транзакций, когда движения записались, а сам документ-регистратор не был создан или был удален "мимо" контроля ссылочной целостности.
Стандартная обработка "Удаление помеченных объектов" пытается прочитать зависимый объект, чтобы принять решение (пометить его на удаление или очистить реквизит). Обращаясь к "пустому" регистратору, она получает ошибку чтения из базы данных, так как объекта с нулевым GUID физически не существует в таблице документов.
Поскольку стандартный поиск не говорит нам, в каком именно регистре зависла запись, нам необходимо использовать более глубокие инструменты. Рассмотрим два пути: использование готовых инструментов и написание собственного скрипта.
Если у вас есть возможность использовать внешние обработки, рекомендуем воспользоваться набором "Инструменты разработчика" (автор Сергей Старых). Это наиболее быстрый способ без написания кода.
РегистрНакопления.ВзаиморасчетыСКонтрагентами), где найдена ссылка, даже если регистратор битый.Если сторонних инструментов под рукой нет, напишем универсальную обработку. Нам нужно перебрать все регистры, которые могут содержать ссылку на наш объект, и проверить записи, у которых регистратор пустой.
Рассмотрим алгоритм на языке 1С. Создайте внешнюю обработку, добавьте реквизит ИскомаяСсылка (любая ссылка) и выполните следующий код:
Процедура НайтиФантомныеЗаписи(ИскомаяСсылка)
// Получаем метаданные конфигурации
МетаданныеКонфигурации = Метаданные;
// Список типов регистров для проверки
МассивКоллекций = Новый Массив;
МассивКоллекций.Добавить(МетаданныеКонфигурации.РегистрыНакопления);
МассивКоллекций.Добавить(МетаданныеКонфигурации.РегистрыБухгалтерии);
МассивКоллекций.Добавить(МетаданныеКонфигурации.РегистрыСведений);
МассивКоллекций.Добавить(МетаданныеКонфигурации.РегистрыРасчета);
Для Каждого Коллекция Из МассивКоллекций Цикл
Для Каждого МетаРегистр Из Коллекция Цикл
// Пропускаем независимые регистры сведений, так как у них нет регистратора (обычно)
// Но если проблема в измерении независимого регистра - это покажет и обычный поиск.
// Нас интересуют зависимые записи.
Если МетаРегистр = Метаданные.РегистрыСведений.Найти(МетаРегистр.Имя)
И МетаРегистр.РежимЗаписи = РежимЗаписиРегистраСведений.Независимый Тогда
Продолжить;
КонецЕсли;
ИмяРегистра = МетаРегистр.Имя;
ПолноеИмя = МетаРегистр.ПолноеИмя();
// Формируем текст запроса динамически
ТекстЗапроса = "ВЫБРАТЬ ПЕРВЫЕ 10 Регистратор ИЗ " + ПолноеИмя + " ГДЕ ";
Условия = Новый Массив;
ЕстьСсылка = Ложь;
// Проверяем измерения, ресурсы и реквизиты на наличие искомого типа
Для Каждого Поле Из МетаРегистр.Измерения Цикл
Если Поле.Тип.СодержитТип(ИскомаяСсылка.Метаданные()) Тогда
Условия.Добавить(Поле.Имя + " = &ИскомаяСсылка");
ЕстьСсылка = Истина;
КонецЕсли;
КонецЦикла;
// (Аналогичные циклы нужны для Ресурсов и Реквизитов, если они есть)
// Для краткости примера опустим их, но в реальном коде их нужно добавить.
Если Не ЕстьСсылка Тогда
Продолжить;
КонецЕсли;
// Собираем условия через ИЛИ
ТекстГде = СтрСоединить(Условия, " ИЛИ ");
ТекстЗапроса = ТекстЗапроса + "(" + ТекстГде + ")";
// САМОЕ ГЛАВНОЕ: Добавляем условие на пустой регистратор
// Обратите внимание: для разных типов регистров проверка может отличаться,
// но обычно проверяем на Неопределено или NULL при левом соединении,
// либо проверяем, что Регистратор.Ссылка ЕСТЬ NULL (если это допускает язык запросов для конкретной таблицы)
// Упрощенный вариант проверки "сирот":
// Мы ищем записи с нашим договором. Потом программно проверим регистратор.
Запрос = Новый Запрос;
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("ИскомаяСсылка", ИскомаяСсылка);
Попытка
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Проверяем регистратор
РегистраторБит = Ложь;
Если Выборка.Регистратор = Неопределено Тогда
РегистраторБит = Истина;
ИначеЕсли Выборка.Регистратор.Пустая() Тогда
// Если ссылка пустая, но тип определен - это тоже подозрительно для движений
РегистраторБит = Истина;
КонецЕсли;
Если РегистраторБит Тогда
Сообщить("НАЙДЕНО! Регистр: " + ИмяРегистра + ". Проблема с регистратором.");
// Здесь можно вызвать процедуру лечения
КонецЕсли;
КонецЦикла;
Исключение
// Игнорируем ошибки доступа
КонецПопытки;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Когда мы выяснили имя регистра (например, РегистрНакопления.Продажи), где зависла запись с пустым регистратором и ссылкой на наш Договор, нам нужно её удалить.
Удаление таких записей — операция деликатная. Поскольку у записи нет нормального регистратора, мы не можем получить набор записей через Набор.Отбор.Регистратор.Установить(...) обычным способом, если регистратор Неопределено.
Однако, если регистратор является "Пустой ссылкой" определенного типа (как в случае автора темы — пустая ссылка на Авансовый отчет), мы можем использовать это.
Алгоритм лечения:
НайтиПоСсылкам (или который мы вычислили).
// Пример очистки для регистра накопления
Процедура ОчиститьБитыйРегистр(ИмяРегистра, ТипДокументаРегистратора)
Набор = РегистрыНакопления[ИмяРегистра].СоздатьНаборЗаписей();
// Создаем пустую ссылку нужного типа
ПустаяСсылкаРегистратора = Новый(ТипДокументаРегистратора); // Например, Документы.АвансовыйОтчет.ПустаяСсылка()
Набор.Отбор.Регистратор.Установить(ПустаяСсылкаРегистратора);
// Считываем. Если записи есть - мы их нашли.
Набор.Прочитать();
Если Набор.Количество() > 0 Тогда
Сообщить("Удаляем " + Набор.Количество() + " фантомных записей в регистре " + ИмяРегистра);
Набор.Очистить();
Набор.Записать(); // Записываем пустой набор, тем самым удаляя записи из БД
Сообщить("Успешно очищено.");
Иначе
Сообщить("Записей с пустым регистратором этого типа не найдено.");
КонецЕсли;
КонецПроцедуры
Если программный метод 1С не помогает (например, платформа падает при попытке чтения набора с пустым регистратором), а доступ к SQL Server имеется, проблему можно решить прямым запросом. Это крайняя мера, требующая осторожности.
Внимание! Перед любыми действиями в SQL обязательно сделайте полную резервную копию базы данных (.bak).
ПолучитьСтруктуруХраненияБазыДанных() в 1С)._IDRRef (или аналогичное поле ссылки) вашего Договора в таблице базы данных._RecorderRRef (ссылка на регистратор) заполнено нулями (0x00000000...), а в полях измерений встречается ID вашего договора.DELETE. Так как эти записи не привязаны к живым объектам ("сироты"), их удаление на уровне SQL обычно безопасно для целостности остальных данных.После удаления фантомных записей обязательно выполните следующие действия: