Как очистить ссылку в связанном документе при удалении его из табличной части?

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

В разработке на платформе 1С часто возникает задача синхронизации связанных данных. Классический пример: есть Документ1, в его табличной части содержатся ссылки на экземпляры Документа2. Для удобства в самом Документе2 хранится обратная ссылка на Документ1, которому он принадлежит. Проблема возникает, когда пользователь удаляет строку из табличной части Документа1 — обратная ссылка в Документе2 "зависает" и указывает на уже неактуальную связь.

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

Решение 1: Сравнение табличной части "До" и "После". Классический подход

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

Алгоритм действий:

  1. Получение исходных данных. Перед записью документа, в обработчике события ПередЗаписью, нам нужно получить "старую" версию табличной части, которая сейчас сохранена в базе. Для этого мы обращаемся к свойству ЭтотОбъект.Ссылка и получаем объект из базы данных. Важно предусмотреть случай, когда документ новый и ссылки еще нет.
  2. Сравнение и обработка. После того как платформа попытается записать объект, в обработчике ПриЗаписи, мы сравниваем сохраненный "старый" список ссылок с текущим состоянием табличной части. Те ссылки, что были в старом списке, но отсутствуют в новом, и есть наши удаленные строки.
  3. Обновление связанных документов. Проходим в цикле по списку удаленных ссылок, для каждой получаем объект связанного Документа2, очищаем у него реквизит с обратной ссылкой и записываем изменения.

Давайте посмотрим на пример реализации в модуле объекта Документ1.


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)

    // Если это новый документ, то сравнивать не с чем, выходим.
    Если ЭтоНовый() Тогда
        Возврат;
    КонецЕсли;

    // Получаем текущее состояние документа из базы данных
    // Чтобы не выполнять лишний запрос, можно использовать более экономный способ
    // через запрос к табличной части по ссылке.
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ИмяТЧ.СсылкаНаДокумент2
        |ИЗ
        |    Документ.Документ1.ИмяТЧ КАК ИмяТЧ
        |ГДЕ
        |    ИмяТЧ.Ссылка = &Ссылка";

    Запрос.УстановитьПараметр("Ссылка", ЭтотОбъект.Ссылка);

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


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

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

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

Решение 2: Использование регистра сведений для контроля связей

Часто за задачей "очистить ссылку" скрывается более глубокая бизнес-потребность: обеспечить уникальность использования Документа2, то есть не позволить выбрать один и тот же Документ2 в табличных частях разных экземпляров Документа1. Попытка решить это через проверку заполненности реквизита в Документе2 — неэффективна и медленна. Придется перебирать все документы, чтобы найти "свободные".

Гораздо более правильное и масштабируемое решение — использовать для хранения связей регистр сведений.

Архитектура решения:

  1. Создаем подчиненный регистр сведений, назовем его СвязиДокументов. Регистратором будет наш Документ1.
  2. Измерения:
    • Документ1 (тип ДокументСсылка.Документ1)
    • Документ2 (тип ДокументСсылка.Документ2)
  3. Из Документа2 убираем реквизит с обратной ссылкой. Вся информация о связях теперь будет храниться централизованно в регистре.

Как это работает:

Пример кода для проверки уникальности в событии ПриИзменении в табличной части на форме:


&НаКлиенте
Процедура ИмяТЧСсылкаНаДокумент2ПриИзменении(Элемент, СтрокаДанных)
    ПроверитьУникальностьДокумента2НаСервере(СтрокаДанных.СсылкаНаДокумент2, СтрокаДанных.НомерСтроки);
КонецПроцедуры

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

Решение 3: Асинхронная обработка через регламентные задания

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

Суть метода:

  1. В момент записи Документа1 (используя метод сравнения "до" и "после") мы не обновляем Документ2 напрямую.
  2. Вместо этого мы создаем запись в специальной очереди — например, в другом регистре сведений "ОчередьОбновленияСвязей". В запись помещаем информацию о том, в каком Документе2 нужно очистить ссылку.
  3. Регламентное задание с определенной периодичностью (раз в несколько минут) запускается, читает эту очередь и порциями обрабатывает накопившиеся изменения.

Преимущества:

Недостатки:

Выводы и рекомендации

Проанализировав ситуацию, можно дать следующие рекомендации:

  1. Для решения простой технической задачи по очистке обратной ссылки используйте классический метод сравнения табличной части до и после записи (Решение 1). Это надежно и соответствует стандартам разработки 1С.
  2. Если ваша реальная цель — это контроль уникальности или построение сложных связей между объектами, не раздумывая переходите на архитектуру с использованием регистра сведений (Решение 2). Это более правильный, производительный и масштабируемый подход, который решает проблему на корню, а не борется с её последствиями.
  3. Асинхронный подход (Решение 3) — это узкоспециализированный инструмент для оптимизации в высоконагруженных системах, где допустима задержка в синхронизации данных.
← На главную