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