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