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