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