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