При разработке конфигураций на платформе 1С:Предприятие 8.3 с использованием управляемых форм мы часто сталкиваемся с необходимостью поддерживать целостность данных между связанными элементами. Одна из таких распространенных задач — обеспечение автоматического удаления строк в подчиненной табличной части при удалении соответствующих родительских строк в главной табличной части того же документа. Давайте рассмотрим эту проблему подробнее, проанализируем различные подходы и выясним, какой из них является наиболее надежным.
Предположим, у нас есть документ, имеющий сложную структуру с многоуровневыми табличными частями. Одна табличная часть (назовем ее Главная ТЧ) содержит основные данные, а другая (Подчиненная ТЧ) содержит строки, которые логически связаны с определенными строками Главной ТЧ. Связь обычно реализуется через поле типа УИД (уникальный идентификатор), которое хранится в Подчиненной ТЧ и ссылается на УИД строки из Главной ТЧ.
Первое, что приходит в голову при необходимости отреагировать на удаление строки в форме, — это использование клиентских обработчиков событий табличной части формы: ПередУдалением и ПослеУдаления.
Обработчик ПередУдалением вызывается непосредственно перед тем, как система удалит выделенные пользователем строки из табличной части формы. На этот момент у нас есть доступ к информации о том, какие строки пользователь собирается удалить.
ПередУдалением мы можем получить доступ к коллекции ВыделенныеСтроки табличной части формы. Эта коллекция содержит ссылки на те строки, которые пользователь выделил и намерен удалить. Мы можем пройти по этим строкам, собрать их уникальные идентификаторы (УИДы) или другие ключевые данные.ПередУдалением заключается в его недостаточной надежности. Если в конце процедуры ПередУдалением (или в другом обработчике этого же события, который может быть определен в расширении или базовой конфигурации) будет установлено значение Отказ = Истина, операция удаления родительских строк будет отменена. Однако, если наша логика по удалению подчиненных строк уже была выполнена (особенно если это был серверный вызов, который модифицировал данные в базе), мы столкнемся с несогласованностью данных: родительские строки останутся, а их подчиненные данные будут удалены.Несмотря на возможность получить информацию о выделенных строках, необходимость гарантировать целостность данных делает ПередУдалением не лучшим выбором для инициации каскадного удаления, если существует хоть малейший шанс отмены операции.
Обработчик ПослеУдаления вызывается после того, как строки уже были успешно удалены из табличной части формы. Казалось бы, это идеальное место, поскольку удаление родительских строк уже произошло, и мы можем спокойно удалить подчиненные.
ПослеУдаления не предоставляет информации о том, какие именно строки были удалены. Свойства табличной части формы, такие как ТекущаяСтрока, ТекущиеДанные или ВыделенныеСтроки, будут пусты или иметь значение Неопределено. Система уже "забыла", что именно было удалено.ПередУдалением, мы не сможем определить, для каких родительских строк нужно удалить подчиненные. Это делает ПослеУдаления неподходящим местом для реализации логики каскадного удаления без дополнительных ухищрений, которые, в свою очередь, могут быть ненадежными (например, сохранение в клиентской переменной модуля не гарантирует, что данные не будут изменены другим обработчиком).Наиболее надежный и устойчивый к ошибкам подход к обеспечению целостности данных, включая каскадное удаление связанных элементов, реализуется на серверной стороне, в обработчиках событий объектов конфигурации. Это гарантирует, что данные останутся согласованными независимо от того, как они изменяются – через пользовательский интерфейс, программно или через внешние интеграции.
Это один из самых распространенных и рекомендуемых способов обеспечить целостность данных при работе с табличными частями. Логика заключается в том, чтобы при каждой записи объекта в базу данных проверять наличие родительских строк для всех подчиненных строк.
Почему это надежно:
ПриЗаписи (или ПередЗаписью) объекта вызывается каждый раз, когда объект (например, документ) сохраняется в базу данных. Это означает, что проверка целостности данных будет выполнена всегда.Как это работает (пошаговая реализация):
Мы будем сравнивать текущее состояние табличных частей с тем, каким оно должно быть, и удалять "осиротевшие" подчиненные строки.
ПриЗаписи (или ПередЗаписью), мы сначала должны собрать уникальные идентификаторы (УИДы) всех строк Главной ТЧ, которые в данный момент присутствуют в объекте и будут записаны. Это наша "белая книга" разрешенных родителей.Пример кода (псевдокод для модуля объекта):
// В модуле объекта, например, в обработчике ПриЗаписи()
// или ПередЗаписью()
Процедура ПриЗаписи(Отказ)
// Предполагаем, что у нашего объекта есть две табличные части:
// 1. "ГлавнаяТЧ" (родительская) с уникальным полем "УИДСтрокиГлавнойТЧ"
// (это поле можно создавать при добавлении новой строки, например,
// через Новый УникальныйИдентификатор())
// 2. "ПодчиненнаяТЧ" (дочерняя) с полем "СсылкаНаРодителя" типа "УИД",
// которое хранит УИД строки из ГлавнойТЧ.
// Шаг 1: Собираем УИДы всех строк главной ТЧ, которые остались
// Используем Соответствие для быстрого поиска
ПереданныеИдентификаторыРодителей = Новый Соответствие();
Для Каждого СтрокаГлавной Из ЭтотОбъект.ГлавнаяТЧ Цикл
// Убедимся, что УИД строки существует. Если строка новая,
// возможно, его нужно сгенерировать, если это не делается автоматически.
// Для существующих строк УИД уже есть.
Если НЕ ЗначениеЗаполнено(СтрокаГлавной.УИДСтрокиГлавнойТЧ) Тогда
// Если для новой строки УИД еще не присвоен, пропустим ее или присвоим.
// В реальной ситуации УИД нужно генерировать при создании строки или
// брать из свойства НоваяСтрока.УИД, если это ТЧ формы.
// При записи объекта, все новые строки ТЧ уже имеют свои УИДы, если они объявлены.
Продолжить;
КонецЕсли;
ПереданныеИдентификаторыРодителей.Вставить(СтрокаГлавной.УИДСтрокиГлавнойТЧ, Истина);
КонецЦикла;
// Шаг 2: Собираем строки подчиненной ТЧ, которые нужно удалить
// (те, у которых нет соответствующего родителя)
СтрокиДляУдаления = Новый Массив();
Для Каждого СтрокаПодчиненной Из ЭтотОбъект.ПодчиненнаяТЧ Цикл
// Если идентификатор родителя не найден в списке УИДов Главной ТЧ,
// значит, родительская строка была удалена.
Если Не ПереданныеИдентификаторыРодителей.Получить(СтрокаПодчиненной.СсылкаНаРодителя) Тогда
СтрокиДляУдаления.Добавить(СтрокаПодчиненной);
КонецЕсли;
КонецЦикла;
// Шаг 3: Удаляем найденные "осиротевшие" строки из ПодчиненнойТЧ
Для Каждого СтрокаУдаления Из СтрокиДляУдаления Цикл
ЭтотОбъект.ПодчиненнаяТЧ.Удалить(СтрокаУдаления);
КонецЦикла;
КонецПроцедуры
Оптимизация производительности:
При очень большом количестве строк в табличных частях циклическая обработка коллекций 1С может быть не самой производительной. Для повышения скорости работы, особенно если подчиненные данные хранятся в отдельных регистрах или справочниках (а не как строки текущей табличной части), рассмотрите использование пакетных операций удаления с помощью запросов.
Обработчик ОбработкаУдаления вызывается на сервере перед тем, как объект будет фактически удален из базы данных. Он предназначен для реализации логики каскадного удаления связанных объектов или записей в других таблицах, которые ссылаются на удаляемый объект.
ОбработкаУдаления не подходит. Он срабатывает только при удалении *всего объекта*, а не при изменении его табличных частей.Если вы работаете с типовой конфигурацией и не имеете возможности или желания изменять ее напрямую, отличным решением будет использование расширений. Мы можем добавить подписку на событие ПриЗаписи (или ПередЗаписью) нужного документа в расширении. В этой подписке реализуется вся описанная выше логика по удалению "осиротевших" строк.
Преимущества:
ПриЗаписи/ПередЗаписью) являются идеальным местом для этого. Это гарантирует, что данные останутся согласованными независимо от того, как они изменяются – через пользовательский интерфейс, программно или через внешние интеграции.ПередУдалением для каскадного удаления из-за риска отмены, теоретически можно собрать УИДы удаляемых родительских строк в ПередУдалением на клиенте, а затем выполнить серверный вызов, передав эти УИДы для удаления подчиненных данных на сервере. Однако этот подход все равно несет риск, если интерактивное удаление родительской строки будет отменено уже *после* того, как наш серверный вызов удалил дочерние строки. Это может привести к несогласованности. Поэтому этот комбинированный подход менее надежен, чем чисто серверный ПриЗаписи.Удалить() для каждой строки в коллекции может быть медленным. В таких случаях гораздо эффективнее использовать пакетные операции удаления с помощью запросов 1С. Мы можем сформировать запрос, который по списку УИДов удаленных родительских строк найдет и удалит все связанные подчиненные записи.Таким образом, для надежного и устойчивого к изменениям решения задачи каскадного удаления строк подчиненной табличной части при удалении родительской строки, мы рекомендуем использовать обработчик события ПриЗаписи (или ПередЗаписью) в модуле объекта. Этот подход обеспечивает максимальную целостность данных и независимость от клиентских манипуляций пользователя.