Как правильно удалять связанные элементы справочника при отмене проведения документа в 1С?

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

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

В этой статье мы подробно разберем, как правильно реализовать отмену проведения документа с удалением связанных объектов, проанализируем типичные ошибки и рассмотрим наиболее оптимальные подходы с точки зрения производительности и стандартов разработки 1С.

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

Для начала давайте разберем типичный пример неоптимального и ошибочного кода, который часто пытаются использовать на практике:


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

В этом фрагменте кода содержится сразу несколько критических ошибок, которые заблокируют корректную работу системы:

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

Решение №1. Использование пометки на удаление и проверка ссылочной целостности

Правильным и безопасным подходом является не физическое удаление элемента справочника, а установка пометки на удаление с помощью метода УстановитьПометкуУдаления(Истина). Это позволит пользователям при необходимости восстановить данные, а также предотвратит появление битых ссылок.

Давайте посмотрим на пример реализации, где мы предварительно проверяем, используется ли элемент где-то еще в системе, с помощью метода НайтиСсылки():


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

Решение №2. Оптимизация производительности (Запрос вместо поиска в цикле)

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

Давайте разберем этот паттерн по шагам:


Процедура ОбработкаУдаленияПроведения(Отказ)
    
    // Шаг 1. Собираем уникальные серийные номера из табличной части в массив
    МассивНомеров = Новый Массив;
    Для Каждого Стр Из ТЧПоступление Цикл
        Если Стр.ТипТехники = Перечисления.ТипТехники.Компьютер И ЗначениеЗаполнено(Стр.СерийныйНомер) Тогда
            Если МассивНомеров.Найти(Стр.СерийныйНомер) = Неопределено Тогда
                МассивНомеров.Добавить(Стр.СерийныйНомер);
            КонецЕсли;
        КонецЕсли;
    КонецЦикла;
    
    Если МассивНомеров.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // Шаг 2. Выполняем один пакетный запрос к базе данных
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |	Карточки.Ссылка КАК Ссылка
        |ИЗ
        |	Справочник.КарточкаКомпьютера КАК Карточки
        |ГДЕ
        |	Карточки.Код В(&МассивНомеров)
        |	И НЕ Карточки.ПометкаУдаления";
        
    Запрос.УстановитьПараметр("МассивНомеров", МассивНомеров);
    РезультатЗапроса = Запрос.Выполнить();
    
    Если НЕ РезультатЗапроса.Пустой() Тогда
        Выборка = РезультатЗапроса.Выбрать();
        
        // Шаг 3. В цикле обрабатываем только найденные ссылки
        Пока Выборка.Следующий() Цикл
            Попытка
                ОбъектСправочника = Выборка.Ссылка.ПолучитьОбъект();
                ОбъектСправочника.УстановитьПометкуУдаления(Истина, Ложь);
            Исключение
                Отказ = Истина;
                Сообщение = Новый СообщениеПользователю;
                Сообщение.Текст = "Не удалось пометить на удаление карточку компьютера из-за блокировки!";
                Сообщение.Сообщить();
            КонецПопытки;
        КонецЦикла;
    КонецЕсли;
    
КонецПроцедуры

Решение №3. Хранение ссылки на созданный объект в самой ТЧ

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

В момент проведения документа мы создаем элемент справочника и сразу же записываем полученную ссылку в соответствующую строку табличной части документа. При отмене проведения нам больше не нужно выполнять поиск по коду или серийному номеру — мы сразу имеем прямую ссылку на нужный объект базы данных.

Рассмотрим, как будет выглядеть процедура отмены проведения в этом случае:


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

Важное примечание: Так как процедура ОбработкаУдаленияПроведения выполняется в транзакции, изменение реквизитов табличной части самого документа в этот момент делать не нужно (документ просто переходит в состояние "не проведен"). Но изменения, внесенные в справочник КарточкаКомпьютера через вызов метода Записать() или УстановитьПометкуУдаления(), зафиксируются в базе данных только в случае успешного завершения всей транзакции отмены проведения.

Решение №4. Архитектурная альтернатива: Регистры сведений

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

Выясним преимущества такого подхода:

Использование механизмов платформы по умолчанию (регистров движений) — это наиболее производительный, надежный и рекомендуемый стандарт разработки 1С, который позволяет полностью избежать проблем с транзакциями, блокировками таблиц и ручным контролем целостности данных.

← На главную