Как найти и заменить реквизиты документа с ссылкой на определенный объект в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

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

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

Анализ исходной задачи и выявление архитектурных ошибок

Рассмотрим сценарий, предложенный автором темы на форуме. У нас есть документ, в табличной части которого содержится список других документов. При изменении контрагента в текущем документе требуется найти все упоминания старого контрагента в связанных документах и заменить их на нового.

В исходном коде автора содержатся критические ошибки, которые могут привести к нестабильной работе системы, блокировкам базы данных и потере целостности данных:

  1. Модификация данных в обработчике ПриИзменении на форме. Выполнение тяжелых серверных транзакций записи в интерактивном событии изменения элемента формы — крайне опасная практика. Если пользователь случайно выберет не того контрагента, система мгновенно перезапишет всю цепочку документов в базе данных. Отменить это действие стандартным закрытием формы без сохранения будет невозможно.
  2. Использование тяжелого метода ПолучитьОбъект() без предварительной проверки. Для каждого документа из списка выполняется чтение всей структуры объекта из базы данных в память сервера. Если связанных документов много, это вызовет колоссальную нагрузку на сервер и СУБД.
  3. Использование РеквизитФормыВЗначение на клиенто-серверной границе. Постоянная конвертация данных формы в прикладной объект и обратно создает избыточный трафик между сервером приложений и базой данных.
  4. Отсутствие обработки табличных частей. Код автора предполагает поиск только в реквизитах шапки, игнорируя тот факт, что контрагент может находиться в табличных частях (например, в списках участников, спецификациях или дополнительных реквизитах).

Разберем по шагам, как исправить эти ошибки и построить надежное, масштабируемое решение.

Шаг 1. Перенос логики в транзакционные события (ПередЗаписью)

Вместо выполнения перезаписи связанных документов в интерактивном режиме на форме, перенесем всю логику в модуль объекта основного документа — в обработчик ПередЗаписью. Это гарантирует, что изменения запишутся только тогда, когда пользователь осознанно нажмет кнопку «Провести и закрыть» или «Записать».

Для определения факта изменения контрагента мы сравним новое значение реквизита с тем, которое уже записано в базе данных. Для этого воспользуемся эффективным методом БСП ОбщегоНазначения.ЗначениеРеквизитаОбъекта, который считывает значение из базы данных с помощью быстрого SQL-запроса, не загружая сам объект в память.

Рассмотрим структуру обработчика в модуле объекта:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Исключаем выполнение при обмене данными
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Проверяем, изменен ли контрагент
    Если ЗначениеЗаполнено(Ссылка) Тогда
        СтарыйКонтрагент = ОбщегоНазначения.ЗначениеРеквизитаОбъекта(Ссылка, "Контрагент");
        
        Если СтарыйКонтрагент <> Контрагент И ЗначениеЗаполнено(Контрагент) Тогда
            // Запускаем процедуру обновления связанных документов
            ОбновитьКонтрагентаВЦепочкеДокументов(СтарыйКонтрагент, Контрагент);
        КонецЕсли;
    КонецЕсли;
    
КонецПроцедуры

Шаг 2. Программный обход реквизитов шапки через метаданные

Теперь реализуем процедуру поиска и замены значений. Поскольку типы связанных документов могут быть разными, мы не можем жестко прописать имена реквизитов в коде. Нам необходимо обратиться к коллекции метаданных объекта.

Для доступа к реквизитам шапки мы будем использовать коллекцию Метаданные().Реквизиты. Прежде чем читать значение конкретного реквизита, мы обязательно проверим его тип с помощью метода СодержитТип(). Это избавит систему от необходимости считывать тяжелые типы данных (длинные строки, хранилища значений), в которых ссылка на контрагента физически не может находиться.

Посмотрим на пример реализации обхода реквизитов шапки связанного документа:


Процедура ОбновитьКонтрагентаВДокументе(СвязанныйДокументСсылка, СтарыйКонтрагент, НовыйКонтрагент)
    
    // Получаем объект для редактирования
    ДокОбъект = СвязанныйДокументСсылка.ПолучитьОбъект();
    Если ДокОбъект = Неопределено Тогда
        Возврат; // Документ мог быть удален другим пользователем
    КонецЕсли;
    
    Модифицирован = Ложь;
    МетаданныеДокумента = ДокОбъект.Метаданные();
    ТипКонтрагента = Тип("СправочникСсылка.Контрагенты");
    
    // 1. Обход реквизитов шапки
    Для Каждого Реквизит Из МетаданныеДокумента.Реквизиты Цикл
        
        // Проверяем, может ли данный реквизит содержать ссылку на контрагента
        Если Реквизит.Тип.СодержитТип(ТипКонтрагента) Тогда
            ИмяРеквизита = Реквизит.Имя;
            
            // Если значение совпадает со старым контрагентом - производим замену
            Если ДокОбъект[ИмяРеквизита] = СтарыйКонтрагент Тогда
                ДокОбъект[ИмяРеквизита] = НовыйКонтрагент;
                Модифицирован = Истина;
            КонецЕсли;
        КонецЕсли;
        
    КонецЦикла;
    
    // Запись объекта будет выполнена ниже после анализа табличных частей
    Если Модифицирован Тогда
        Попытка
            ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
        Исключение
            ЗаписьЖурналаРегистрации("ОбновлениеЦепочкиДокументов",
                УровеньЖурналаРегистрации.Ошибка,,,
                "Не удалось записать документ " + СвязанныйДокументСсылка + ". Описание: " + ОписаниеОшибки());
        КонецПопытки;
    КонецЕсли;

КонецПроцедуры

Шаг 3. Реализация поиска и замены в табличных частях

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

Алгоритм устроен следующим образом: сначала мы перебираем все табличные части, описанные в метаданных документа. Для каждой табличной части мы анализируем ее колонки (реквизиты табличной части) и отбираем только те, которые поддерживают тип СправочникСсылка.Контрагенты. И только если такие колонки найдены, мы запускаем цикл по строкам этой табличной части. Это существенно оптимизирует быстродействие алгоритма.

Дополним нашу процедуру ОбновитьКонтрагентаВДокументе логикой обхода табличных частей:


// ... (продолжение процедуры ОбновитьКонтрагентаВДокументе)

    // 2. Обход табличных частей
    Для Каждого ТЧ Из МетаданныеДокумента.ТабличныеЧасти Цикл
        
        // Собираем список колонок ТЧ, которые имеют тип "Контрагент"
        КолонкиСКонтрагентом = Новый Массив;
        Для Каждого РеквизитТЧ Из ТЧ.Реквизиты Цикл
            Если РеквизитТЧ.Тип.СодержитТип(ТипКонтрагента) Тогда
                КолонкиСКонтрагентом.Добавить(РеквизитТЧ.Имя);
            КонецЕсли;
        КонецЦикла;
        
        // Если в данной ТЧ есть колонки с нужным типом, обходим строки
        Если КолонкиСКонтрагентом.Количество() > 0 Тогда
            ТабличнаяЧастьДокумента = ДокОбъект[ТЧ.Имя];
            
            Для Каждого СтрокаТЧ Из ТабличнаяЧастьДокумента Цикл
                Для Каждого ИмяКолонки Из КолонкиСКонтрагентом Цикл
                    Если СтрокаТЧ[ИмяКолонки] = СтарыйКонтрагент Тогда
                        СтрокаТЧ[ИмяКолонки] = НовыйКонтрагент;
                        Модифицирован = Истина;
                    КонецЕсли;
                КонецЦикла;
            КонецЦикла;
        КонецЕсли;
        
    КонецЦикла;

    // Записываем документ, если были внесены изменения
    Если Модифицирован Тогда
        Попытка
            // Используем стандартный метод записи или метод БСП ОбщегоНазначения.ЗаписатьОбъект
            ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
        Исключение
            ВызватьИсключение "Ошибка при записи связанного документа " + СвязанныйДокументСсылка + ": " + ОписаниеОшибки();
        КонецПопытки;
    КонецЕсли;

Шаг 4. Объединение логики для всей цепочки документов

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

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


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

Альтернативный подход: Использование платформенного метода НайтиПоСсылкам

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

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


// Поместим ссылку на старого контрагента в массив
МассивСсылок = Новый Массив;
МассивСсылок.Добавить(СтарыйКонтрагент);

// Ищем все места использования в базе данных
ТаблицаСсылок = НайтиПоСсылкам(МассивСсылок);

// ТаблицаСсылок содержит колонки:
// Ссылка - объект, в котором найдена ссылка
// Данные - метаданные реквизита, где найдена ссылка
Для Каждого СтрокаТаблицы Из ТаблицаСсылок Цикл
    
    // Фильтруем только интересующие нас документы
    Если ОбщегоНазначения.ЭтоДокумент(СтрокаТаблицы.Ссылка) Тогда
        // Здесь можно реализовать точечную замену
        // СтрокаТаблицы.Данные указывает точный путь к реквизиту
    КонецЕсли;
    
КонецЦикла;

Преимущество метода НайтиПоСсылкам: Он работает на уровне СУБД крайне быстро и гарантирует, что вы найдете абсолютно все упоминания объекта, исключая пропуск данных.

Недостаток: Метод ищет ссылки во всей базе данных. Если контрагент старый и активно использовался, количество найденных ссылок может исчисляться тысячами, что сделает интерактивную замену слишком долгой.

Использование стандартных обработок БСП

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

В любой современной конфигурации на базе БСП уже встроены готовые инструменты администратора:

Использование штатных инструментов БСП экономит время разработчика и гарантирует соблюдение всех бизнес-правил и подписок на события при перезаписи документов.

← На главную