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