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