Почему СрезПоследних не возвращает новые цены в ОбработкеПроведения и как это исправить?

Программист 1С v8.3 (Управляемые формы) 1C:Бухгалтерия IT и автоматизация бизнеса
← На главную

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

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

Выясняем причину: жизненный цикл движений документа

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

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

Таким образом, когда мы пытаемся выполнить запрос к виртуальной таблице СрезПоследних непосредственно внутри тела процедуры ОбработкаПроведения, физических записей в таблице регистра в базе данных еще нет! Они находятся только в оперативной памяти сервера приложений 1С в виде незаписанной коллекции Движения. Запрос, выполняемый к СУБД, обращается к таблицам базы данных и, естественно, возвращает пустоту при первом проведении.

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

Решение «в лоб» и его скрытые опасности

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

Рассмотрим пример такого кода:


Процедура ОбработкаПроведения(Отказ, Режим)
    
    // Формируем движения по регистру ЦеныНоменклатуры
    Движения.ЦеныНоменклатуры.Записывать = Истина;
    
    Для Каждого СтрокаЦен Из ТаблицаЦен Цикл
        Движение = Движения.ЦеныНоменклатуры.Добавить();
        Движение.Период = Дата;
        Движение.Номенклатура = СтрокаЦен.Номенклатура;
        Движение.Цена = СтрокаЦен.Цена;
        Движение.Валюта = Валюта;
        Движение.ТипЦен = ТипЦен;
    КонецЦикла;
    
    // Принудительно записываем движения в базу данных в рамках транзакции
    Движения.ЦеныНоменклатуры.Записать();
    
    // Теперь СрезПоследних увидит эти записи
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
        |   ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
        |ИЗ
        |   РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ТипЦен = &ТипЦен) КАК ЦеныНоменклатурыСрезПоследних";
    
    Запрос.УстановитьПараметр("Период", Период);
    Запрос.УстановитьПараметр("ТипЦен", ТипЦен);
    
    РезультатЗапроса = Запрос.Выполнить();
    // Дальнейшая обработка данных...
    
КонецПроцедуры

Этот код действительно решит проблему: виртуальная таблица СрезПоследних вернет актуальные данные, поскольку метод Движения.ЦеныНоменклатуры.Записать() физически отправил данные в СУБД в рамках открытой транзакции.

Однако этот подход категорически не рекомендуется стандартами разработки фирмы «1С» (Стандарт № 450) по нескольким критически важным причинам:

Правильные архитектурные альтернативы

Рассмотрим, как решить задачу корректно, производительно и в соответствии со стандартами разработки «1С».

Вариант 1. Работа с данными в оперативной памяти (Рекомендуемый способ)

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

Разберем по шагам, как получить новые цены без обращения к СУБД:


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

КонецПроцедуры

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

Вариант 2. Получение среза последних ДО записи новых движений

Очень часто программисту требуется получить предыдущие (старые) цены из базы данных, чтобы сравнить их с новыми ценами из проводимого документа (например, для контроля критического изменения цены или записи истории отклонений).

В этом случае правильный алгоритм выглядит так:

  1. Сначала мы выполняем запрос к виртуальной таблице СрезПоследних, передавая в параметры дату документа (или момент времени). На этом этапе мы получаем «старое» состояние базы данных, так как новые движения еще даже не начинали формироваться.
  2. Выгружаем результат запроса со старыми ценами в таблицу значений в оперативной памяти.
  3. Программно сопоставляем старые цены из таблицы значений с новыми ценами из табличной части документа.
  4. Записываем результаты анализа или формируем окончательные движения.

Посмотрим на пример реализации такого подхода:


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

КонецПроцедуры

Резюме и рекомендации

Для создания надежных, отказоустойчивых и быстродействующих конфигураций на платформе 1С:Предприятие 8 всегда придерживайтесь следующих правил транзакционной работы:

← На главную